Compare commits

..

94 Commits

Author SHA1 Message Date
5adb1e807b spinner 2026-06-06 17:00:22 +03:00
0b2425f506 fix 2026-06-06 16:55:48 +03:00
8487ac5ca0 fix 2026-06-06 16:23:55 +03:00
517059d53e fix 2026-06-06 15:53:14 +03:00
d1e6529950 admin page 2026-06-06 10:28:21 +03:00
d296032e90 admin page 2026-06-05 22:58:52 +03:00
da55c61edd admin page 2026-06-05 22:52:52 +03:00
f4af2fd137 admin page 2026-06-05 22:33:02 +03:00
fd66ca9c9b admin page 2026-06-05 16:13:04 +03:00
a85f9aabd5 admin page 2026-06-05 15:22:37 +03:00
d812b8b44a admin page 2026-06-05 14:36:43 +03:00
3892a459e5 admin page 2026-06-05 14:36:12 +03:00
27f125cbe8 admin page 2026-06-05 14:31:31 +03:00
0e92966a5d admin page 2026-06-05 12:46:05 +03:00
6a399ea7ca f 2026-06-02 00:20:38 +03:00
5b915bbc22 f 2026-06-01 23:39:28 +03:00
0409d63874 f 2026-06-01 23:33:55 +03:00
b86f3209f5 f 2026-06-01 23:28:57 +03:00
895bec2a50 f 2026-05-30 15:02:00 +03:00
9b1d6ffb5d refactor(converter): shared page layout + reusable conversion logic/UI
Pages:
- add WalletLayout route (WalletHeader + main + Footer via <Outlet/>),
  wrap converter/swap/bridge/transactions; thin pages, drop duplicated shell CSS
- extract SwapBridgeTabs shared between swap/bridge pages

Converter reuse (FSD layers, no widget->widget imports):
- move commission tiers to entities/commission (+ CommissionTable ui)
- shared calc hook features/payment/model/useCurrencyConversion;
  useConverterSection becomes thin wrapper; HomePage Converter reuses it
- move ConvertField/DirectionSwapButton to shared/ui; delete dead useConverter

Tooling:
- add eslint.config.js (ESLint 9 flat config); fix no-explicit-any in WalletPage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:39:53 +03:00
bdc8bd3d93 add redirects 2026-05-29 15:12:22 +03:00
efbb94b43d add redirects 2026-05-28 22:32:13 +03:00
8dfd48fe52 add redirects 2026-05-28 22:27:06 +03:00
71b799dbf3 add redirects 2026-05-28 20:32:24 +03:00
2026230ff6 add redirects 2026-05-28 20:24:56 +03:00
2e6ed487fd add redirects 2026-05-27 20:33:33 +03:00
acd30dbb13 convert 2026-05-25 20:36:06 +03:00
b3831273d6 convert 2026-05-25 15:23:01 +03:00
91282ba908 convert 2026-05-25 15:20:38 +03:00
5e71e8b7c9 convert 2026-05-25 15:14:10 +03:00
433c275f40 F 2026-05-22 23:12:46 +03:00
96ea3788d5 F 2026-05-22 23:11:09 +03:00
aa25c6dec5 F 2026-05-22 22:57:05 +03:00
61c50eecc1 F 2026-05-22 22:40:26 +03:00
f425cef139 F 2026-05-22 22:36:10 +03:00
52a0b7f3c7 F 2026-05-22 22:34:00 +03:00
fac5e2ea5e F 2026-05-22 22:14:15 +03:00
e9d1b733a5 19.05.2026 okkk 2026-05-19 21:41:20 +03:00
8a5ff01619 19.05.2026 okkk 2026-05-19 21:35:28 +03:00
39897c5bfe 19.05.2026 okkk 2026-05-19 21:28:52 +03:00
c1cdf835af 19.05.2026 okkk 2026-05-19 21:24:27 +03:00
1ec70fcab8 19.05.2026 okkk 2026-05-19 21:20:03 +03:00
29fbd71d8f 19.05.2026 okkk 2026-05-19 21:03:21 +03:00
fad50a1b1b 19.05.2026 okkk 2026-05-19 16:01:32 +03:00
7907ff0def f 2026-05-19 15:53:20 +03:00
370141c83b 19.05.2026 okkk 2026-05-19 15:50:47 +03:00
36196a882f f 2026-05-19 15:10:42 +03:00
22ceb8e2b4 19.05.2026 okkk 2026-05-19 14:38:35 +03:00
2a48d4a4c5 17.05.2026 funny 2026-05-17 14:56:59 +03:00
01d72f4885 17.05.2026 funny 2026-05-17 14:45:28 +03:00
2d1c1654f9 17.05.2026 funny 2026-05-17 14:31:14 +03:00
ed5c7ea79d 17.05.2026 funny 2026-05-17 14:16:56 +03:00
b0dc637b8f 17.05.2026 funny 2026-05-17 14:03:33 +03:00
b5791a0871 17.05.2026 funny 2026-05-17 13:10:13 +03:00
bd3d747ede 17.05.2026 funny 2026-05-17 13:06:18 +03:00
73d1fd9135 17.05.2026 funny 2026-05-17 12:41:47 +03:00
253407abc3 17.05.2026 funny 2026-05-17 12:23:32 +03:00
3387769578 14.05.2026 rip 2026-05-15 00:38:21 +03:00
b0a9b3c12a 14.05.2026 rip 2026-05-15 00:29:14 +03:00
272d99a2bd 14.05.2026 rip 2026-05-15 00:23:04 +03:00
3437c6259b 14.05.2026 rip 2026-05-15 00:01:56 +03:00
6fc9f38182 14.05.2026 rip 2026-05-14 23:57:47 +03:00
7c8e812d4b 14.05.2026 rip 2026-05-14 23:39:47 +03:00
0d114e12c2 14.05.2026 rip 2026-05-14 23:23:20 +03:00
26a7315ea1 14.05.2026 rip 2026-05-14 23:05:50 +03:00
51d0b8b3fc 14.05.2026 rip 2026-05-14 22:55:45 +03:00
7038542f14 14.05.2026 rip 2026-05-14 22:51:11 +03:00
c05fa1eaa5 14.05.2026 rip 2026-05-14 22:47:56 +03:00
dc470bf74b 14.05.2026 rip 2026-05-14 22:43:31 +03:00
b2fc051cbb 14.05.2026 rip 2026-05-14 22:42:45 +03:00
cdbd9318cf 14.05.2026 rip 2026-05-14 22:39:06 +03:00
0668ecccf3 14.05.2026 rip 2026-05-14 22:06:48 +03:00
789d99aa12 14.05.2026 rip 2026-05-14 22:04:30 +03:00
ca3dd78783 14.05.2026 rip 2026-05-14 21:49:42 +03:00
c1472d5363 14.05.2026 rip 2026-05-14 21:42:16 +03:00
c2a1ca3ee5 14.05.2026 rip 2026-05-14 21:23:27 +03:00
1e5f792854 14.05.2026 rip 2026-05-14 20:40:10 +03:00
43cd51aa13 14.05.2026 rip 2026-05-14 20:23:13 +03:00
2d58658420 14.05.2026 rip 2026-05-14 20:19:09 +03:00
e837351a6e 14.05.2026 rip 2026-05-14 20:10:51 +03:00
fcfdac87b4 14.05.2026 rip 2026-05-14 20:05:35 +03:00
2de30fbde6 14.05.2026 rip 2026-05-14 18:30:57 +03:00
22bb446309 14.05.2026 rip 2026-05-14 18:05:59 +03:00
168dedcdc5 14.05.2026 rip 2026-05-14 18:03:48 +03:00
5e5fe02bb7 14.05.2026 rip 2026-05-14 17:59:22 +03:00
226371e0f6 14.05.2026 rip 2026-05-14 17:54:58 +03:00
b4502027fa 14.05.2026 rip 2026-05-14 17:47:32 +03:00
926cd5dd06 14.05.2026 rip 2026-05-14 17:36:00 +03:00
d641036327 14.05.2026 rip 2026-05-14 17:23:22 +03:00
8d5dc3a5d1 14.05.2026 rip 2026-05-14 17:17:40 +03:00
4913765584 14.05.2026 rip 2026-05-14 16:32:13 +03:00
9c3fbbdc4b 14.05.2026 rip 2026-05-14 16:15:56 +03:00
f5562871c0 14.05.2026 rip 2026-05-14 15:57:32 +03:00
020bf86404 14.05.2026 rip 2026-05-14 15:55:19 +03:00
248 changed files with 16630 additions and 1349 deletions

File diff suppressed because one or more lines are too long

161
dist/assets/index-CF-a3AIG.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-f9EVcxOv.css vendored Normal file

File diff suppressed because one or more lines are too long

97
dist/assets/popcat-DOGy5LFs.svg vendored Normal file
View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 994.3 1000.5" style="enable-background:new 0 0 994.3 1000.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F7F5F8;}
.st1{fill:#DCC4D4;}
.st2{fill:#EFAD93;}
.st3{fill:#D68C72;}
.st4{fill:#964C49;}
.st5{fill:#3A0101;}
.st6{fill:#894544;}
.st7{fill:#E3746D;}
.st8{fill:#FFFFFF;}
.st9{fill:#623538;}
.st10{fill:#C95755;}
</style>
<g>
<path class="st0" d="M27.4,444.9l471.8-254l124.2-2.5c0,0,129.7,15.8,232.7,116.6c0.1,0.1,0.2,0.1,0.2,0.2
c28.2,27.7,54.4,62.5,75.1,106.3c1.4,2.9,2.8,5.9,4.1,8.9c0,0,151.2,244-41.3,456.4c-192.5,212.3-651.4,110.6-789.3-112.5
c-1-1.5-2-3.1-3-4.7c-21.2-34.1-37-67.9-48.6-99.2c-14.7-39.6-22.9-75.1-27.4-101.6C20.8,528.5,27.5,444.9,27.4,444.9z"/>
<path class="st1" d="M218.8,420.3c-31.3,88.9,7.8,189.9,85.9,99.6c111.2-153,321.4-183.3,405.1-172.5
c102.5,19.8,182.5,123.4,207.9,125.4c25.4,2,13.7-61.3,13.7-61.3c1.4,2.9,2.8,5.9,4.1,8.9c0,0,151.2,244-41.3,456.4
C701.7,1089.1,242.8,987.4,105,764.2c-1-1.5-2-3.1-3-4.7c-21.2-34.1-37-67.9-48.6-99.2C106.8,572.1,243.9,348.5,218.8,420.3z"/>
<path class="st2" d="M680.7,183.7c49.4-48.5,129.9-117.2,190.6-152.4c29.2-17,53.8-26.2,68.1-21.7c2.8,0.8,5.1,2.2,7.1,4.1
c23.7,23.2,14.4,169.4,3.6,279.9c-7,70.6-14.6,126.7-14.6,126.7c-15.5-33.2-32-65.7-81.8-118c-20.7-20.3-41.8-37.4-63.2-51.7
c-36-24-72.5-40.1-108.3-50.3c-4.8-1.4-9.6-2.6-14.4-3.8C671.8,192.5,676.1,188.2,680.7,183.7z"/>
<path class="st3" d="M871.3,31.4c-10.1,73.5-12.2,200.9,78.8,262.3c-7,70.6-14.6,126.7-14.6,126.7c-15.5-33.2-32-65.7-81.8-118
c-20.7-20.3-41.8-37.4-63.2-51.7c-36-24-72.5-40.1-108.3-50.3l-1.6-16.6C730.1,135.2,810.6,66.6,871.3,31.4z"/>
<path class="st4" d="M51.1,14.9c49.7-15.8,213.2,136.5,315.7,216.5c0,0,141.2-74,301.1-34.9c7.5,1.8,15.1,3.9,22.8,6.3
c-46.6-1.7-115.4-3.1-144.9,2.2C499,213.5,329,264,254.7,383.2c-74.3,119.1-71.3,258.8-69.5,320.2c1.4,48.9-34.4,79.5-88.3,48
c-44.5-74.3-63.2-146.8-71-192.6c-1.1-10.1-2.2-20.5-3.5-31c-0.8-6-1.4-12-2.1-17.9C-12.3,223.4,2.5,30.3,51.1,14.9z"/>
<path class="st4" d="M939.4,9.6c-41.2,32.2-128.1,113.5-148.9,241c-40.8-27.2-82.3-44.2-122.7-54.1C736,126.7,892.6-5,939.4,9.6z"
/>
<path class="st5" d="M667.3,196.7c18-21.7,38.7-41.3,59-60.8c30.9-28.7,63.1-56.1,97.5-80.5c23.1-16.3,47-31.5,73.2-42.8
c13.8-5.3,28.2-11.2,43.7-6.9c3.2,0.8,4.1,5.5,1.3,7.4c-48,35.5-89.8,81.2-115,136l-7.3,16.4l-6.3,16.8c-8.3,23.1-13.5,47.2-19,71
c-1.4,2.2-4.4,2.7-6.5,1.3c-9.3-6.2-18.7-12.1-28.3-17.7C730.6,220,699.1,207.3,667.3,196.7L667.3,196.7z M668.3,196.4
c33.5,5.2,65.5,17.4,95.4,33c10,5.2,19.8,11.1,29.3,17.3l-7.2,3.2c11.1-73.1,51.8-137.4,99-192.5c15.9-18.4,33.2-35.6,52-51.1
l1.3,7.4c-34.7-7-111.7,54.4-141.3,75.9C755.4,121.4,714.9,155,676,189.9C673.8,191.8,670.7,194.7,668.3,196.4L668.3,196.4z"/>
<path class="st5" d="M667,196.8c9.8-12.7,21.5-23.8,32.5-35.3c56.3-55.7,115.3-109,185.4-147.2C903.6,5.5,934-9.3,952.6,8
c8.6,9.3,11,23.4,12.9,34.8c7.6,63.5,0.5,127.1-5.9,190.2c-7.1,62.7-14.3,125.2-20.2,188c-0.1,1.2-0.9,2.5-2.1,3.1
c-1.9,1-4.3,0.2-5.3-1.7c-21.3-43.7-51.6-81.8-85.2-116.5c-29-28.5-61.5-53.6-97.1-73.3C723.7,217.7,695.1,206.6,667,196.8
L667,196.8z M668.6,196.3c102.9,16.6,200.3,90.3,251,181.1c7.4,13.4,13.7,27.4,19.6,41.3l-7.6,1.2c15.8-93,21.6-187.6,22.5-281.8
c0.1-30.9,0.3-62.6-4.6-92.8c-1.5-8.2-3.1-16-6.7-22.5c-2.2-3.9-5.1-5.3-10-5.6c-13.2-0.3-28,6.5-40.3,12.5
c-13.6,6.6-26.9,14.5-39.9,22.9c-62,40.5-119.8,87.7-175.4,136.6C674.9,191.3,671.1,194.7,668.6,196.3L668.6,196.3z"/>
<path class="st5" d="M368.1,222.4c0,0-27.7,15.2-48.3,39.2c32.8-26.4,66.9-39.2,66.9-39.2H368.1z"/>
<path class="st5" d="M51.4,15.6C-13.1,43.9,7.5,316.1,13.6,383c3.6,42.3,8.6,84.5,13.7,126.7c0.9,16.6,3.4,33.8,6.5,50.3
c15.8,83.2,49,164.3,101,231.4c162.6,201.4,596.3,285.2,772.8,64.1c93.2-112.3,103.3-255.1,47.5-387.3c-6.8-15.7-14.3-31.3-23-45.7
c-9.8-21.3-21.3-41.7-34.7-60.8C858,307,809.5,265.5,746.8,233.1c-117.3-59-260-50.3-377.9,2.6c-1.6,0.8-3.7,0.7-5.2-0.5
c-50.3-40.8-98.5-83.9-148.5-125C183.7,85.2,89.5,5.3,51.4,15.6L51.4,15.6z M50.9,14.1c38.2-10.9,133.1,68.2,166.8,93
c51.1,39.6,100.6,81.4,152,120.5l-5.2-0.5c10.1-5.3,20.2-9.7,30.6-13.9c113.6-45.9,247.7-50.1,357.9,7.6
c38.5,19.3,80,51.8,106.2,79.6c28.3,26.3,63.5,77.6,79.6,117.9c37.5,68.6,57.7,147.2,55.3,225.5c-2,78.7-32.2,155.1-80.9,216.3
c-144.1,184.7-435.8,164.2-626.6,69.8c-70.3-34.9-136.1-83.3-181-148.9c-52.9-79.3-87.3-177.1-92-270.3
c-12-126.6-19.9-254.6-7-381.4C10.8,98.9,18.7,25.8,50.9,14.1L50.9,14.1z"/>
<path class="st2" d="M56.5,68.9c10.8,4.5,48,42.6,88.9,92c33.1,40,68.6,87.5,94.4,130.5C256.3,318.9,134.9,522,113,560.3
c-20.1,35.1-64.5-92-76.1-226c-1.1-12-1.8-23.9-2.3-35.9C28.8,152.3,36.9,60.8,56.5,68.9z"/>
<path class="st6" d="M327.6,643.2c7.1-85.1,52.7-166.4,117.6-206.6c75-42,139.1-61.4,202.6-61.1l0,0c15.2,0.1,30.5,1.3,45.8,3.6
c15,2.3,30.1,5.6,45.5,9.9c66,18.5,208.6,88.2,186.5,282.8c-0.7,5.8-1.5,11.5-2.5,17.2C891,873.9,688.2,995.2,467.4,887.4
C358,834,319.8,736.3,327.6,643.2z"/>
<path class="st7" d="M636.5,336.4c9.7,2.9,32.2,11.3,56.1-1c23.9-12.3,33.2,15.1,20.5,22.1c-14.4,7.9-18.4,17-19.6,21.7
c-15.3-2.3-30.5-3.5-45.8-3.6l0,0c-4.1-7.4-25.4-8.4-33.2-21.1C606.7,341.7,626.8,333.4,636.5,336.4z"/>
<path class="st8" d="M386.7,314c6.2-5.8,12.6-10.9,19.1-15.3c58-40,121.3-28.3,130.8-3.7c10.6,27.3-35.8,36.7-64.2,48.6
c-25.9,10.8-56.1,30.6-78.7,43.5c-16.6,9.5-29.3,15.3-33.1,11.1C351.5,388.3,342.5,354.9,386.7,314z"/>
<path class="st8" d="M773.8,272.5c33.4,12.4,58.6,33.3,75,50.7c11.4,12.1,18.4,22.5,20.9,27.2c6.1,11.4-12.7,14-36.2,5.3
c-1-0.4-2.1-0.8-3.2-1.2c-25.3-9-84.3-25.6-101.1-33C705.1,310.9,717.2,251.5,773.8,272.5z"/>
<path class="st5" d="M405.8,298.7c58-40,121.3-28.3,130.8-3.7c10.6,27.3-35.8,36.7-64.2,48.6c-25.9,10.8-56.1,30.6-78.7,43.5
C370.3,352.9,386.3,320.8,405.8,298.7z"/>
<path class="st5" d="M773.8,272.5c33.4,12.4,58.6,33.3,75,50.7c0.6,10.9-2.7,23.7-18.5,31.3c-25.3-9-84.3-25.6-101.1-33
C705.1,310.9,717.2,251.5,773.8,272.5z"/>
<path class="st5" d="M504.4,274.3c-72.3-19-136.4,35.7-149,74s-0.5,51.9,0,40.6s0-28.9,18-52.9c18.1-24,76-50.5,76-50.5
S477.1,292.5,504.4,274.3z"/>
<path class="st5" d="M869.5,341.4c-20.4-25.1-54.2-47.2-54.2-47.2s5.1,17,18.1,25.7c13,8.8,37.3,35.8,37.3,35.8
S876.6,352.4,869.5,341.4z"/>
<path class="st8" d="M504.4,294.2c-8.5,0.9-74.6,10.1-65.2-3.7s34.7-12.3,55.5-10.7C517.1,281.5,512.9,293.4,504.4,294.2z"/>
<path class="st8" d="M803.6,314.5c-12.8-6.2-34.7-17.4-31.5-26s27.8,6.4,42.5,14C833.1,312,816.4,320.7,803.6,314.5z"/>
<path class="st9" d="M445.2,436.6c75-42,139.1-61.4,202.6-61.1l0,0c15.2,0.1,30.5,1.3,45.8,3.6c15,2.3,30.1,5.6,45.5,9.9
c66,18.5,208.6,88.2,186.5,282.7c-0.7,5.8-1.5,11.5-2.5,17.2C859.5,755.1,757.8,798,643,797.9c-139.3,0-259.6-63.2-315.5-154.7
C334.6,558.1,380.2,476.8,445.2,436.6z"/>
<path class="st5" d="M467,888.1c-4.1-2.1-15-8-19-10.1c-4.3-2.5-13.8-8.5-18.2-11.3c-5.7-3.9-11.5-8.7-17.3-12.6
c-12.8-10.7-25.3-22-35.9-35c-51-59.2-67.4-143.5-50-219.2c14.9-66.8,52.5-130.4,110.4-168.2c74.4-43.3,162.1-73,249-60.2
c66.6,9.1,131.6,37.9,178.2,87.3c47.5,48.6,68.8,117.8,66.7,185c-0.4,38.6-7.7,77.3-22.3,113.1C871.9,848.2,785.9,914.7,689,928
C613.2,939.7,535.3,921.3,467,888.1L467,888.1z M467.7,886.6c103.1,51,224,52.6,322.3-10.2c62.5-40,109.6-104.2,126.4-177.1
c12.6-54.7,12.2-114.2-9.7-166.5C878,462.3,811,414,739.3,394.7C633,365.6,539,391.2,446.1,446.3
C361.9,503,321.5,614.9,336.4,714.1c5.5,34.6,19.1,68.2,39.4,96.7c3.2,4,7.6,9.9,10.8,13.9c6.6,7.1,14.9,16.4,22.4,22.6l5.2,4.8
l5.6,4.3c6.3,5.3,16.2,11.9,23.2,16.3C449.7,877.2,460.6,882.6,467.7,886.6L467.7,886.6z"/>
<path class="st10" d="M624.5,349.4c9.9-5,28,14.1,28,14.1C630,361.9,614.5,354.5,624.5,349.4z"/>
<path class="st10" d="M709.7,347.4c-8.3-4.5-19.1,16.1-19.1,16.1C701.2,361.2,718,351.9,709.7,347.4z"/>
<path class="st3" d="M145.4,160.9c33.1,40,68.6,87.5,94.4,130.5c16.5,27.5-104.9,230.6-126.9,268.9c-20.1,35.1-64.5-92-76.1-226
C134.4,390.3,148.1,261.2,145.4,160.9z"/>
<path class="st5" d="M239.3,291.7C203,231.8,156.6,179,109.9,127.3C94.3,110.6,78.5,93.1,61,78.7c-2.4-1.8-4.8-3.7-6.8-4.5
c-2.9,2.5-5.2,9.1-6.4,13.8c-7.9,33.4-8.7,68.6-9.8,103.1c-0.4,23.2-0.5,46.6-0.3,69.9c-0.2,70,5.5,140.1,23.6,207.9
c5.3,20,24.9,86.9,43.7,93.3c1.6-0.1,2.6-1.2,3.6-2.7l0.9-1.5c3.4-7.1,73.1-128.3,78.8-138.8C201.1,395.1,248.5,315.1,239.3,291.7
L239.3,291.7z M240.3,291.1c10.1,18.9-35.6,108.8-46,131.3c-23,47.3-49.2,93.1-76.6,137.9c-2,4.1-7.1,10.6-13.3,9.8
c-30.8-4.3-55.4-138.4-61-168.9C30.8,332,27.6,261.4,27.8,191.1c0.8-28.9-0.2-103.5,17.4-124.3c7.7-8.1,16.3-2.5,22.8,3
c9.4,7.7,17.6,16.1,25.8,24.6C150,154,197.5,221.4,240.3,291.1L240.3,291.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

52
dist/assets/uni-C5oaqT41.svg vendored Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 432.7" style="enable-background:new 0 0 400 432.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F50DB4;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#F50DB4;}
</style>
<path class="st0" d="M325.1,65.4c0.6-10.2,2-17,4.8-23.1c1.1-2.4,2.1-4.4,2.3-4.4s-0.3,1.8-1.1,4c-2,6-2.4,14.2-1,23.7
c1.8,12.1,2.8,13.8,15.6,26.9c6,6.1,13,13.8,15.5,17.1l4.6,6l-4.6-4.3c-5.6-5.3-18.6-15.5-21.4-17c-1.9-1-2.2-1-3.4,0.2
c-1.1,1.1-1.3,2.7-1.5,10.4c-0.2,12-1.9,19.7-5.8,27.3c-2.1,4.2-2.5,3.3-0.5-1.4c1.4-3.5,1.6-5,1.6-16.6c0-23.3-2.8-28.9-19.1-38.5
c-4.1-2.4-10.9-5.9-15.1-7.8c-4.2-1.9-7.5-3.5-7.4-3.6c0.5-0.5,16.3,4.2,22.7,6.6c9.5,3.6,11.1,4.1,12.2,3.7
C324.4,74.2,324.8,72,325.1,65.4z"/>
<path class="st0" d="M124.4,31.5c-5.6-0.9-5.9-1-3.2-1.4c5.1-0.8,17.1,0.3,25.4,2.2c19.3,4.6,36.9,16.3,55.6,37l5,5.5l7.1-1.1
c30-4.8,60.6-1,86.1,10.8c7,3.2,18.1,9.7,19.5,11.3c0.4,0.5,1.3,3.9,1.8,7.5c1.9,12.5,0.9,22.1-2.9,29.3c-2.1,3.9-2.2,5.1-0.8,8.5
c1.1,2.7,4.3,4.6,7.4,4.6c6.3,0,13.2-10.2,16.3-24.4l1.3-5.6l2.5,2.8c13.7,15.4,24.4,36.4,26.2,51.3l0.5,3.9l-2.3-3.5
c-4-6.1-7.9-10.2-13-13.6c-9.2-6-18.9-8.1-44.5-9.4c-23.2-1.2-36.3-3.2-49.3-7.4c-22.1-7.2-33.3-16.7-59.6-51.1
c-11.7-15.2-18.9-23.7-26.1-30.5C161,42.8,145,34.7,124.4,31.5z"/>
<path class="st0" d="M135.4,105.3c-11.4-15.7-18.5-39.7-17-57.6l0.5-5.6l2.6,0.5c4.9,0.9,13.3,4,17.3,6.4
c10.8,6.5,15.5,15.2,20.3,37.3c1.4,6.5,3.2,13.8,4.1,16.3c1.4,4,6.5,13.3,10.7,19.4c3,4.4,1,6.4-5.6,5.8
C158,126.9,144.2,117.5,135.4,105.3z"/>
<path class="st0" d="M311.3,221.9c-53.5-21.4-72.3-40-72.3-71.4c0-4.6,0.2-8.4,0.4-8.4c0.2,0,2.3,1.5,4.6,3.4
c10.8,8.6,23,12.3,56.6,17.2c19.8,2.9,30.9,5.2,41.2,8.6c32.6,10.8,52.8,32.6,57.6,62.4c1.4,8.6,0.6,24.9-1.7,33.4
c-1.8,6.7-7.3,18.9-8.7,19.4c-0.4,0.1-0.8-1.4-0.9-3.5c-0.6-11.2-6.2-22-15.8-30.2C361.4,243.6,346.9,236.2,311.3,221.9z"/>
<path class="st0" d="M273.7,230.8c-0.7-4-1.8-9-2.6-11.3l-1.4-4l2.5,2.8c3.5,3.9,6.3,8.9,8.6,15.6c1.8,5.1,2,6.6,2,14.9
c0,8.1-0.2,9.8-1.9,14.4c-2.6,7.2-5.8,12.4-11.3,17.9c-9.8,9.9-22.3,15.4-40.4,17.6c-3.1,0.4-12.3,1.1-20.4,1.5
c-20.3,1.1-33.7,3.2-45.8,7.4c-1.7,0.6-3.3,1-3.4,0.8c-0.5-0.5,7.7-5.3,14.5-8.6c9.5-4.6,19-7.1,40.3-10.6
c10.5-1.7,21.4-3.9,24.1-4.7C264.6,276.7,278,256.2,273.7,230.8z"/>
<path class="st0" d="M298.2,274.1c-7.1-15.2-8.7-29.9-4.8-43.5c0.4-1.5,1.1-2.7,1.5-2.7c0.4,0,2.1,0.9,3.7,2
c3.3,2.2,9.8,5.9,27.3,15.4c21.8,11.8,34.3,21,42.7,31.5c7.4,9.2,12,19.6,14.2,32.3c1.3,7.2,0.5,24.6-1.3,31.8
c-5.9,22.9-19.5,40.9-39,51.4c-2.9,1.5-5.4,2.8-5.7,2.8c-0.3,0,0.8-2.6,2.3-5.8c6.5-13.6,7.3-26.8,2.3-41.6c-3-9-9.2-20-21.7-38.6
C305.4,287.5,301.8,281.7,298.2,274.1z"/>
<path class="st0" d="M97.4,356.1c19.8-16.7,44.5-28.5,67-32.1c9.7-1.6,25.8-0.9,34.8,1.3c14.4,3.7,27.3,11.9,33.9,21.6
c6.5,9.5,9.3,17.9,12.3,36.4c1.2,7.3,2.4,14.6,2.8,16.3c2.2,9.6,6.5,17.3,11.8,21.1c8.4,6.1,22.9,6.5,37.1,1
c2.4-0.9,4.5-1.6,4.7-1.4c0.5,0.5-6.7,5.3-11.7,7.8c-6.8,3.4-12.2,4.7-19.4,4.7c-13,0-23.9-6.6-32.9-20c-1.8-2.6-5.8-10.6-8.9-17.6
c-9.5-21.6-14.2-28.2-25.3-35.4c-9.6-6.3-22.1-7.4-31.4-2.8c-12.3,6-15.7,21.6-6.9,31.5c3.5,3.9,10,7.3,15.3,8
c10,1.2,18.5-6.3,18.5-16.3c0-6.5-2.5-10.2-8.8-13.1c-8.6-3.9-17.9,0.7-17.9,8.7c0,3.4,1.5,5.6,5,7.2c2.2,1,2.3,1.1,0.5,0.7
c-7.9-1.6-9.8-11.1-3.4-17.4c7.7-7.6,23.5-4.2,28.9,6.1c2.3,4.3,2.5,13,0.6,18.2c-4.5,11.7-17.4,17.8-30.6,14.5
c-9-2.3-12.6-4.7-23.4-15.8c-18.8-19.3-26.1-23-53.2-27.2l-5.2-0.8L97.4,356.1z"/>
<path class="st1" d="M9.2,11.5c62.8,75.7,106,107,110.8,113.6c4,5.5,2.5,10.4-4.3,14.2c-3.8,2.1-11.5,4.3-15.4,4.3
c-4.4,0-5.9-1.7-5.9-1.7c-2.6-2.4-4-2-17.1-25.1C59.1,88.8,43.9,65.5,43.5,65.1c-1-0.9-0.9-0.9,32,57.7c5.3,12.2,1.1,16.7,1.1,18.4
c0,3.5-1,5.4-5.4,10.3c-7.3,8.1-10.6,17.2-12.9,36.1c-2.6,21.1-10.1,36.1-30.7,61.6c-12.1,15-14,17.7-17.1,23.7
c-3.8,7.6-4.9,11.9-5.3,21.4c-0.5,10.1,0.4,16.7,3.5,26.4c2.7,8.5,5.6,14.1,12.9,25.3c6.3,9.7,9.9,16.9,9.9,19.7
c0,2.2,0.4,2.2,10.2,0.1c23.3-5.2,42.2-14.4,52.8-25.7c6.6-7,8.1-10.8,8.2-20.4c0-6.3-0.2-7.6-1.9-11.2c-2.8-5.9-7.8-10.7-18.9-18.3
c-14.5-9.9-20.8-17.9-22.5-28.8c-1.4-9,0.2-15.3,8.3-32.1c8.3-17.4,10.4-24.8,11.8-42.3c0.9-11.3,2.2-15.8,5.4-19.3
c3.4-3.7,6.5-5,14.9-6.1c13.7-1.9,22.5-5.4,29.7-12c6.2-5.7,8.9-11.2,9.3-19.5l0.3-6.3l-3.5-4C122.8,105.1,0.8,0,0,0
C-0.2,0,4,5.2,9.2,11.5z M38.5,305.9c2.9-5,1.3-11.5-3.4-14.7c-4.5-3-11.5-1.6-11.5,2.3c0,1.2,0.7,2.1,2.1,2.8
c2.5,1.3,2.7,2.7,0.7,5.7c-2,3-1.8,5.6,0.5,7.4C30.5,312.3,35.7,310.7,38.5,305.9z"/>
<path class="st1" d="M147.6,164.9c-6.5,2-12.7,8.8-14.7,15.9c-1.2,4.4-0.5,12,1.3,14.3c2.9,3.8,5.6,4.8,13.1,4.8
c14.7-0.1,27.5-6.4,29-14.2c1.2-6.4-4.4-15.3-12.1-19.2C160.2,164.4,151.7,163.6,147.6,164.9z M164.8,178.3c2.3-3.2,1.3-6.7-2.6-9
c-7.3-4.5-18.4-0.8-18.4,6.1c0,3.4,5.8,7.2,11.1,7.2C158.4,182.6,163.2,180.5,164.8,178.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
<script type="module" crossorigin src="/assets/index-Hdj79d7b.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-3J8Zo9Sf.css">
<script type="module" crossorigin src="/assets/index-CF-a3AIG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-f9EVcxOv.css">
</head>
<body>
<div id="root"></div>

28
eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

37
package-lock.json generated
View File

@@ -11,8 +11,10 @@
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.100.9",
"axios": "^1.7.9",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-easy-crop": "^5.5.7",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5",
"zod": "^3.24.1"
@@ -3055,6 +3057,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
"license": "BSD-3-Clause"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3216,6 +3224,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
@@ -3237,6 +3254,20 @@
"react": "^19.2.5"
}
},
"node_modules/react-easy-crop": {
"version": "5.5.7",
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.7.tgz",
"integrity": "sha512-kYo4NtMeXFQB7h1U+h5yhUkE46WQbQdq7if54uDlbMdZHdRgNehfvaFrXnFw5NR1PNoUOJIfTwLnWmEx/MaZnA==",
"license": "MIT",
"dependencies": {
"normalize-wheel": "^1.0.1",
"tslib": "^2.0.1"
},
"peerDependencies": {
"react": ">=16.4.0",
"react-dom": ">=16.4.0"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -3516,6 +3547,12 @@
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -13,8 +13,10 @@
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.100.9",
"axios": "^1.7.9",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-easy-crop": "^5.5.7",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5",
"zod": "^3.24.1"

255
politika-cookie.txt Normal file
View File

@@ -0,0 +1,255 @@
ПОЛИТИКА ИСПОЛЬЗОВАНИЯ ФАЙЛОВ COOKIE
====================================
Общие положения и терминология
------------------------------
Настоящая Политика использования файлов cookie (далее — Политика) устанавливает порядок обработки файлов cookie и содержащихся в них персональных данных ООО «БИТФОРС» (далее — Оператор, мы) при использовании пользователями (далее — Субъекты персональных данных, вы) интернет-ресурса https://bitforce-foundation.ru.
Файлы cookie — это текстовые файлы небольшого размера, которые устанавливаются на пользовательское устройство (телефон, компьютер, планшет) при посещении интернет-ресурса или совершении на нем определенных действий. Файлы cookie остаются сохраненными на устройстве даже после покидания ресурса, что позволяет «узнавать» пользователя при последующих посещениях.
К персональным данным относится не сам файл cookie, а его содержимое — уникальные идентификаторы, IP-адреса, информация о предпочтениях пользователя и другие данные, позволяющие прямо или косвенно идентифицировать физическое лицо.
Оператор персональных данных
----------------------------
Оператором персональных данных, содержащихся в файлах cookie, является:
ООО «БИТФОРС»
ИНН: 9810001062
ОГРН: 1257800060990
Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н
Оператор определяет цели обработки персональных данных, их состав, а также действия (операции) с персональными данными, включая случаи использования сторонних файлов cookie (third-party cookies), формат и механика работы которых определены третьими лицами (например, Яндекс.Метрика).
Категории файлов cookie и их назначение
---------------------------------------
На нашем интернет-ресурсе используются следующие категории файлов cookie:
1. Строго необходимые (технические) файлы cookie
------------------------------------------------
Данные файлы обеспечивают работу интернет-ресурса и предоставление необходимого уровня сервиса: авторизацию, навигацию, отображение контента в соответствии с параметрами устройства, обеспечение безопасности.
Обработка таких файлов cookie осуществляется на основании п. 5 ч. 1 ст. 6 ФЗ № 152 (заключение и исполнение договора) — Пользовательского соглашения, устанавливающего правила использования интернет-ресурса. Согласие на использование строго необходимых файлов cookie не требуется.
Примеры: файлы сессий (PHPSESSID), настройки безопасности, файлы аутентификации.
2. Функциональные файлы cookie
------------------------------
Используются для запоминания пользовательских предпочтений и персонализации взаимодействия с сайтом: сохранение выбранного языка, региона, настроек отображения, размера шрифта.
Обработка осуществляется на основании согласия субъекта персональных данных (п. 1 ч. 1 ст. 6 ФЗ № 152), поскольку данная обработка не является строго необходимой для функционирования сайта.
Примеры: настройки языка интерфейса, предпочтения отображения, настройки доступности.
3. Аналитические файлы cookie
-----------------------------
Собирают информацию о взаимодействии пользователей с интернет-ресурсом для анализа его использования, выявления популярных разделов, обнаружения ошибок и улучшения пользовательского опыта. Могут содержать персональные данные, включая IP-адреса пользователей.
Обработка осуществляется на основании согласия субъекта персональных данных (п. 1 ч. 1 ст. 6 ФЗ № 152), так как данная обработка используется Оператором для получения конкурентного преимущества и не связана напрямую с предоставлением пользователю сервиса.
4. Маркетинговые файлы cookie
-----------------------------
Используются для отслеживания пользователей в целях персонализированной рекламы, анализа эффективности рекламных кампаний, ретаргетинга. Обрабатывают персональные данные пользователей.
Обработка осуществляется исключительно на основании согласия субъекта персональных данных (п. 1 ч. 1 ст. 6 ФЗ № 152).
Примеры: пиксели социальных сетей, рекламные идентификаторы, файлы ретаргетинга.
Правовые основания обработки персональных данных
------------------------------------------------
Обработка персональных данных, содержащихся в файлах cookie, осуществляется на следующих правовых основаниях в соответствии со ст. 6 ФЗ № 152:
1. Согласие субъекта персональных данных (п. 1 ч. 1 ст. 6) — для функциональных, аналитических и маркетинговых файлов cookie. Согласие должно удовлетворять критериям конкретности, предметности, информированности, сознательности и однозначности в соответствии с ч. 1 ст. 9 ФЗ № 152.
2. Заключение и исполнение договора (п. 5 ч. 1 ст. 6) — для строго необходимых файлов cookie, обеспечивающих работу интернет-ресурса и исполнение Пользовательского соглашения.
3. Законные интересы оператора (п. 7 ч. 1 ст. 6) — в исключительных случаях, когда отсутствуют иные основания и при условии, что не нарушаются права и свободы субъекта персональных данных.
Важно: В соответствии с п. 5 ч. 1 ст. 6 ФЗ № 152 запрещается включать в договор положения, допускающие бездействие субъекта персональных данных в качестве формы согласия.
Порядок получения согласия
--------------------------
Согласие на обработку персональных данных, содержащихся в файлах cookie, получается в соответствии с требованиями ФЗ № 152 и практикой Роскомнадзора:
Принципы получения согласия:
• Согласие должно быть получено до начала обработки персональных данных;
• Информация об использовании файлов cookie размещается на первом уровне интернет-ресурса (всплывающее уведомление);
• Предоставляется возможность выбора категорий файлов cookie;
• Используются активные формулировки ("используя сайт", "продолжая пользоваться сайтом");
• Исключаются пассивные формулировки ("оставаясь на сайте", "находясь на сайте").
Критерии действительного согласия:
• Добровольность — согласие дается по свободной воле субъекта;
• Конкретность — четко определены цели обработки;
• Информированность — предоставлена полная информация об обработке;
• Однозначность — согласие выражено в недвусмысленной форме.
Сторонние файлы cookie
----------------------
Использование сторонних сервисов:
Наш интернет-ресурс использует файлы cookie сторонних сервисов, включая:
• Яндекс.Метрика (ООО «ЯНДЕКС», Россия);
• Социальные сети и сервисы интеграции.
Обеспечение защиты:
• Получено согласие на передачу;
• Применяются дополнительные меры защиты данных;
• Контролируется соблюдение принципов обработки персональных данных получателями.
Ответственность за сторонние файлы cookie:
Оператор несет ответственность за использование сторонних файлов cookie в соответствии с законодательством о персональных данных, поскольку определяет цели и средства их обработки на своем интернет-ресурсе.
Сроки обработки и хранения
--------------------------
Сроки обработки персональных данных, содержащихся в файлах cookie, определяются целями обработки и требованиями законодательства:
Категории по срокам хранения:
• Сеансовые cookie — удаляются автоматически при закрытии браузера;
• Постоянные cookie — хранятся установленный период или до удаления пользователем.
Конкретные сроки:
• Необходимые файлы cookie — до 12 месяцев;
• Функциональные файлы cookie — до 12 месяцев;
• Аналитические файлы cookie — до 24 месяцев;
• Маркетинговые файлы cookie — до 24 месяцев.
Автоматическое удаление:
По истечении установленных сроков файлы cookie удаляются автоматически. Пользователь может удалить файлы cookie досрочно через настройки браузера или отозвать согласие на их обработку.
Права субъектов персональных данных
-----------------------------------
В соответствии с ФЗ № 152 субъекты персональных данных имеют следующие права:
Право на информацию (ст. 14 ФЗ № 152):
• Получение информации о обработке персональных данных;
• Сведения о правовых основаниях и целях обработки;
• Информация о сроках обработки и составе данных.
Право на доступ (ст. 14 ФЗ № 152):
• Получение подтверждения факта обработки;
• Ознакомление с обрабатываемыми персональными данными;
• Получение информации об источниках персональных данных.
Право на уточнение, блокирование, удаление (ст. 14 ФЗ № 152):
• Требование уточнения неточных данных;
• Блокирование недостоверных данных;
• Удаление незаконно полученных данных.
Право на отзыв согласия:
• Отзыв согласия в любое время;
• Прекращение обработки после отзыва согласия;
• Сохранение права на обжалование действий оператора.
Право на обжалование:
• Обращение в Роскомнадзор или его территориальные органы;
• Обращение в суд для защиты нарушенных прав;
• Требование возмещения ущерба.
Способы управления файлами cookie
---------------------------------
Управление через настройки сайта:
• Использование баннера согласия на файлы cookie;
• Изменение настроек в любое время через интерфейс сайта;
• Отзыв согласия на использование отдельных категорий файлов cookie.
Управление через браузер:
Большинство браузеров позволяют контролировать файлы cookie:
• Блокировка — запрет установки новых файлов cookie;
• Удаление — очистка существующих файлов cookie;
• Уведомления — получение предупреждений при установке файлов cookie;
• Селективная настройка — разрешение файлов cookie только для определенных сайтов.
Инструкции для популярных браузеров:
• Google Chrome: Настройки → Конфиденциальность и безопасность → Файлы cookie
• Mozilla Firefox: Настройки → Приватность и Защита → Файлы cookie
• Safari: Настройки → Конфиденциальность → Файлы cookie
• Microsoft Edge: Настройки → Файлы cookie и разрешения сайтов
Важное замечание: Блокировка необходимых файлов cookie может привести к ограничению функциональности интернет-ресурса.
Меры безопасности
-----------------
Оператор применяет правовые, организационные и технические меры для защиты персональных данных в соответствии с требованиями ФЗ № 152 и постановления Правительства РФ № 1119:
Правовые меры:
• Назначение ответственного за организацию обработки персональных данных;
• Ознакомление сотрудников с требованиями законодательства;
• Заключение соглашений о неразглашении персональных данных.
Организационные меры:
• Определение перечня лиц, допущенных к обработке персональных данных;
• Установление правил доступа к персональным данным;
• Контроль за соблюдением требований по защите персональных данных.
Технические меры:
• Использование средств защиты информации;
• Применение криптографических средств защиты;
• Обеспечение целостности и доступности персональных данных;
• Регулярное обновление систем защиты информации.
Обновления политики
-------------------
Настоящая Политика может изменяться в связи с:
• Изменениями в законодательстве Российской Федерации;
• Изменениями в практике Роскомнадзора;
• Развитием технологий обработки данных;
• Изменениями в бизнес-процессах Оператора.
Процедура уведомления об изменениях:
• Размещение обновленной Политики на интернет-ресурсе;
• Указание даты последнего обновления;
• Уведомление пользователей о существенных изменениях через интерфейс сайта;
• При необходимости — получение нового согласия на обработку.
Рекомендуем регулярно проверять данную страницу для ознакомления с актуальной информацией.
Контактная информация и обращения
---------------------------------
Для реализации прав субъекта персональных данных обращайтесь к нам:
ООО «БИТФОРС»
ИНН: 9810001062
ОГРН: 1257800060990
Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н
Контакты:
• Email компании: company@bitforcefoundation.ru
Порядок рассмотрения обращений:
• Срок рассмотрения обращений — до 30 дней с момента получения;
• Обращения рассматриваются в письменной форме;
• Ответ направляется способом, указанным в обращении;
• При отказе в удовлетворении требований указываются мотивированные основания.
Обращения в контролирующие органы:
Федеральная служба по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор)

View File

@@ -0,0 +1,549 @@
ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ
======================================
ООО «БИТФОРС»
1. Общие положения
------------------
1.1. Настоящая Политика обработки персональных данных (далее — Политика) разработана в соответствии с Федеральным законом от 27.07.2006 № 152-ФЗ «О персональных данных» (далее — Закон о персональных данных) и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые ООО «БИТФОРС» (далее — Оператор).
1.2. Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты права на неприкосновенность частной жизни, личную и семейную тайну.
1.3. Настоящая Политика действует в отношении всех персональных данных, которые обрабатываются Оператором с использованием средств автоматизации и без использования таких средств, при условии, что обработка персональных данных без использования средств автоматизации соответствует характеру действий (операций), совершаемых с персональными данными с использованием средств автоматизации.
1.4. Основные понятия
---------------------
Автоматизированная обработка персональных данных — обработка персональных данных с помощью средств вычислительной техники.
Блокирование персональных данных — временное прекращение обработки персональных данных (за исключением случаев, если обработка необходима для уточнения персональных данных).
Веб-сайт — совокупность графических и информационных материалов, а также программ для ЭВМ и баз данных, обеспечивающих их доступность в сети интернет по сетевому адресу https://bitforce-foundation.ru.
Информационная система персональных данных — совокупность содержащихся в базах данных персональных данных и обеспечивающих их обработку информационных технологий и технических средств.
Обезличивание персональных данных — действия, в результате которых невозможно определить без использования дополнительной информации принадлежность персональных данных конкретному субъекту персональных данных.
Обработка персональных данных — любое действие (операция) или совокупность действий (операций), совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, передачу (распространение, предоставление, доступ), обезличивание, блокирование, удаление, уничтожение персональных данных.
Оператор — государственный орган, муниципальный орган, юридическое или физическое лицо, самостоятельно или совместно с другими лицами организующие и/или осуществляющие обработку персональных данных, а также определяющие цели обработки персональных данных, состав персональных данных, подлежащих обработке, действия (операции), совершаемые с персональными данными.
Персональные данные — любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных).
Персональные данные, разрешенные субъектом персональных данных для распространения — персональные данные, доступ неограниченного круга лиц к которым предоставлен субъектом персональных данных путем дачи согласия на обработку персональных данных, разрешенных субъектом персональных данных для распространения.
Пользователь — любой посетитель веб-сайта https://bitforce-foundation.ru.
Предоставление персональных данных — действия, направленные на раскрытие персональных данных определенному лицу или определенному кругу лиц.
Распространение персональных данных — действия, направленные на раскрытие персональных данных неопределенному кругу лиц (передача персональных данных) или на ознакомление с персональными данными неограниченного круга лиц, в том числе обнародование персональных данных в средствах массовой информации, размещение в информационно-телекоммуникационных сетях или предоставление доступа к персональным данным каким-либо иным способом.
Уничтожение персональных данных — действия, в результате которых невозможно восстановить содержание персональных данных в информационной системе персональных данных и/или результате которых уничтожаются материальные носители персональных данных.
2. Сведения об операторе
------------------------
2.1. Полное наименование оператора: Общество с ограниченной ответственностью «БИТФОРС»
2.2. Сокращенное наименование: ООО «БИТФОРС»
2.3. ИНН: 9810001062
2.4. ОГРН: 1257800060990
2.5. Реестр ОПД: Посмотреть в реестре Роскомнадзора (https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn=)
2.6. Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н
2.7. Почтовый адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н
2.8. Контактная информация:
• Электронная почта: company@bitforcefoundation.ru
Веб-сайт: https://bitforce-foundation.ru
2.9. Ответственный за организацию обработки персональных данных: назначается приказом генерального директора
2.10. Контакты ответственного лица по вопросам обработки персональных данных:
• Электронная почта: company@bitforcefoundation.ru
3. Общие цели обработки персональных данных
-------------------------------------------
3.1. Оператор обрабатывает персональные данных для достижения следующих общих целей:
3.1.1. Основная деятельность:
• Предоставление услуг по конвертации иного имущества;
• Осуществление операций на криптовалютных рынках;
• Предоставление услуг в области блокчейн технологий;
• Обеспечение функционирования интернет-платформы и мобильных приложений.
3.1.2. Обеспечение безопасности:
• Предотвращение мошенничества и отмывания денежных средств;
• Обеспечение безопасности платежных операций;
• Выполнение требований по противодействию легализации доходов, полученных преступным путем;
• Идентификация и верификация клиентов.
3.1.3. Соблюдение законодательства:
• Исполнение требований российского и международного законодательства;
• Взаимодействие с контролирующими и правоохранительными органами;
• Ведение обязательной отчетности и документооборота;
• Соблюдение налогового законодательства.
3.1.4. Развитие бизнеса:
• Анализ и улучшение качества предоставляемых услуг;
• Разработка новых продуктов и сервисов;
• Проведение маркетинговых исследований;
• Осуществление клиентской поддержки.
4. Цели сбора персональных данных
---------------------------------
4.1. Регистрация и идентификация пользователей:
• Создание учетной записи на веб-сайте;
• Верификация личности в соответствии с требованиями законодательства;
• Подтверждение права на осуществление операций;
• Обеспечение доступа к персональному кабинету.
4.2. Обработка платежей и финансовых операций:
• Осуществление операций по конвертации криптовалют;
• Проведение расчетов и переводов денежных средств;
• Ведение учета и истории транзакций;
• Расчет комиссий и сборов.
4.3. Обеспечение безопасности и соблюдение требований:
• Проверка на причастность к запрещенной деятельности;
• Мониторинг подозрительных операций;
• Выполнение процедур по противодействию легализации доходов;
• Архивирование данных для правоохранительных органов.
4.4. Коммуникация с клиентами:
• Предоставление технической поддержки;
• Уведомления о состоянии операций и счетов;
• Информирование об изменениях в условиях предоставления услуг;
• Рассылка маркетинговых материалов (при наличии согласия).
4.5. Аналитика и улучшение сервиса:
• Анализ пользовательского поведения для улучшения интерфейса;
• Создание аналитических отчетов;
• Исследование рынка и потребностей клиентов;
• Разработка персонализированных предложений.
4.6. Исполнение договорных обязательств:
• Выполнение условий пользовательского соглашения;
• Предоставление заказанных услуг;
• Рассмотрение жалоб и претензий;
• Урегулирование споров.
5. Правовые основания обработки персональных данных
---------------------------------------------------
5.1. Правовыми основаниями обработки персональных данных Оператором являются:
5.1.1. Согласие субъекта персональных данных (п. 1 ч. 1 ст. 6 Закона о персональных данных):
• Обработка персональных данных в маркетинговых целях;
• Использование файлов cookie и метрик;
• Персонализация сервисов и предложений;
• Проведение опросов и исследований.
5.1.2. Необходимость исполнения договора (п. 5 ч. 1 ст. 6 Закона о персональных данных):
• Регистрация и ведение учетных записей пользователей;
• Осуществление финансовых операций и переводов;
• Предоставление доступа к платформе и сервисам;
• Техническая поддержка и обслуживание клиентов.
5.1.3. Соблюдение правовой обязанности (п. 2 ч. 1 ст. 6 Закона о персональных данных):
• Выполнение требований валютного законодательства;
• Противодействие легализации доходов, полученных преступным путем;
• Соблюдение требований по налоговому учету и отчетности;
• Взаимодействие с правоохранительными органами.
5.1.4. Защита жизненно важных интересов (п. 3 ч. 1 ст. 6 Закона о персональных данных):
• Предотвращение мошенничества и финансовых преступлений;
• Обеспечение безопасности платежных систем;
• Защита от кибератак и несанкционированного доступа.
5.1.5. Законные интересы оператора (п. 7 ч. 1 ст. 6 Закона о персональных данных):
• Обеспечение информационной безопасности;
• Аналитика для улучшения качества услуг;
• Защита прав и законных интересов Оператора;
• Взыскание задолженности.
5.2. При обработке персональных данных на основании согласия субъекта, такое согласие оформляется в соответствии с требованиями ст. 9 Закона о персональных данных.
5.3. При обработке персональных данных на основании законных интересов Оператор обеспечивает баланс между своими интересами и правами субъектов персональных данных.
6. Объем и категории обрабатываемых персональных данных
-------------------------------------------------------
6.1. Категории субъектов персональных данных:
6.1.1. Пользователи веб-сайта и мобильного приложения:
• Зарегистрированные пользователи;
• Посетители сайта без регистрации;
• Потенциальные клиенты;
• Бывшие клиенты.
6.1.2. Клиенты:
• Физические лица, пользующиеся услугами Оператора;
• Индивидуальные предприниматели;
• Представители юридических лиц;
• Выгодоприобретатели.
6.1.3. Сотрудники и партнеры:
• Сотрудники Оператора;
• Кандидаты на трудоустройство;
• Представители контрагентов;
• Консультанты и советники.
6.2. Категории и состав обрабатываемых персональных данных:
6.2.1. Идентификационные данные:
• Фамилия, имя, отчество;
• Дата рождения;
• Место рождения;
• Гражданство;
• Пол.
6.2.2. Паспортные данные:
• Серия и номер паспорта;
• Дата выдачи паспорта;
• Код подразделения;
• Кем выдан паспорт;
• Адрес регистрации;
6.2.3. Контактная информация:
• Номера телефонов (мобильный, домашний, рабочий);
• Адреса электронной почты;
6.2.4. Финансовая информация:
• Номера банковских счетов и карт;
• Реквизиты кошельков криптовалют;
• История операций и транзакций;
• Данные о доходах и источниках средств;
• Налоговая информация.
6.2.5. Техническая информация:
• IP-адреса устройств;
• Данные о браузере и операционной системе;
• Файлы cookie и локальное хранилище;
• Логи действий на сайте;
• Геолокационные данные.
6.2.6. Дополнительные данные:
• Фотографии для верификации;
• Биометрические данные (при использовании);
• Видеозаписи видеоидентификации;
• Данные о семейном положении и составе семьи;
• Профессиональная информация.
6.3. Источники получения персональных данных:
• Непосредственно от субъектов персональных данных;
• Из общедоступных источников;
• От третьих лиц с согласия субъекта;
• Из государственных информационных систем;
• От партнеров и контрагентов.
7. Порядок и условия обработки персональных данных
--------------------------------------------------
7.1. Принципы обработки персональных данных:
• Обработка персональных данных осуществляется на законной и справедливой основе;
• Обработка ограничивается достижением конкретных, заранее определенных и законных целей;
Не допускается обработка персональных данных, несовместимая с целями сбора;
• Содержание и объем обрабатываемых персональных данных соответствуют заявленным целям;
• Обрабатываемые персональные данные являются точными и актуальными.
7.2. Способы обработки персональных данных:
7.2.1. Автоматизированная обработка:
Сбор данных через веб-формы и мобильные приложения;
• Автоматическая обработка платежей и транзакций;
• Автоматизированный анализ и мониторинг операций;
• Генерация отчетов и статистики.
7.2.2. Неавтоматизированная обработка:
• Ручная верификация документов;
• Обработка обращений службы поддержки;
• Подготовка документов для регулирующих органов;
• Архивирование документов на электронных носителях (при необходимости на бумажных носителях).
7.3. Условия обработки персональных данных:
7.3.1. Получение согласия:
• Согласие получается в письменной форме или путем совершения конклюдентных действий;
• Субъект информируется о целях, способах и сроках обработки;
• Предоставляется возможность отозвать согласие;
• Ведется учет полученных согласий.
7.3.2. Обработка без согласия:
• Осуществляется только в случаях, предусмотренных законодательством;
• Документируются правовые основания обработки;
• Обеспечивается соответствие объема обработки заявленным целям;
• Ведется учет случаев обработки без согласия.
7.4. Сроки обработки персональных данных:
• Персональные данные обрабатываются в течение времени, необходимого для достижения целей обработки;
• После достижения целей обработки персональные данные подлежат уничтожению или обезличиванию;
• Сроки хранения определяются требованиями законодательства и внутренними регламентами;
• Ведется учет сроков обработки для каждой категории данных.
7.5. Места обработки персональных данных:
• Основные серверы и хранилища данных расположены на территории Российской Федерации;
• Резервные копии могут храниться в дата-центрах на территории РФ;
• Обработка может осуществляться удаленно с соблюдением требований безопасности;
8. Актуализация, исправление, удаление и уничтожение персональных данных
------------------------------------------------------------
8.1. Актуализация персональных данных:
8.1.1. Обязанности субъекта:
• Предоставление актуальных и достоверных персональных данных;
• Незамедлительное уведомление об изменении персональных данных;
• Подтверждение изменений документально при необходимости.
8.1.2. Процедуры актуализации:
• Регулярная проверка актуальности данных;
• Запросы на подтверждение данных;
• Автоматическое обновление при получении новой информации;
• Верификация изменений через дополнительные каналы связи.
8.2. Исправление персональных данных:
8.2.1. Основания для исправления:
• Обнаружение неточностей в персональных данных;
• Получение запроса от субъекта персональных данных;
• Выявление ошибок при автоматизированной обработке;
• Получение уточняющей информации из достоверных источников.
8.2.2. Процедура исправления:
• Рассмотрение запроса в течение 30 дней;
• Проверка обоснованности требования об исправлении;
• Внесение изменений во все информационные системы;
• Уведомление субъекта о проведенных исправлениях;
• Уведомление третьих лиц, которым передавались неточные данные.
8.3. Удаление персональных данных:
8.3.1. Основания для удаления:
• Отзыв согласия субъектом персональных данных;
• Достижение целей обработки;
• Истечение сроков обработки;
• Выявление незаконности обработки;
• Требование субъекта при наличии оснований.
8.3.2. Процедура удаления:
• Проверка наличия законных оснований для продолжения обработки;
• Удаление из всех информационных систем и баз данных;
• Удаление резервных копий (кроме архивных);
• Уведомление субъекта о выполненном удалении;
• Документирование факта удаления.
8.4. Уничтожение персональных данных:
8.4.1. Случаи уничтожения:
• Истечение сроков хранения;
• Ликвидация организации;
• Прекращение обработки по решению суда;
• Техническая необходимость (замена оборудования).
8.4.2. Способы уничтожения:
• Физическое уничтожение носителей информации;
• Криптографическое уничтожение (удаление ключей шифрования);
• Перезапись информации на носителях;
• Размагничивание магнитных носителей;
• Использование специализированного программного обеспечения.
8.4.3. Документирование уничтожения:
• Составление актов уничтожения;
• Ведение журналов уничтожения;
• Фиксация времени, способа и ответственных лиц;
• Хранение документов об уничтожении в течение установленного срока.
9. Ответы на запросы субъектов персональных данных
--------------------------------------------------
9.1. Права субъектов персональных данных:
9.1.1. Право на информацию (ст. 14 Закона о персональных данных):
• Подтверждение факта обработки персональных данных;
• Правовые основания и цели обработки;
• Применяемые способы обработки;
• Наименование и местонахождение оператора;
• Лица, имеющие доступ к персональным данным;
• Обрабатываемые персональные данные;
• Источник получения персональных данных;
• Сроки обработки персональных данных;
• Порядок осуществления прав субъекта;
9.1.2. Право на доступ:
• Получение копий обрабатываемых персональных данных;
• Ознакомление с историей обработки;
• Получение информации о передаче данных третьим лицам;
• Доступ к автоматизированным решениям.
9.1.3. Право на исправление:
• Требование исправления неточных данных;
• Дополнение неполных данных;
• Актуализация устаревших данных.
9.1.4. Право на удаление ("право на забвение"):
• Требование удаления персональных данных;
• Отзыв согласия на обработку;
• Возражение против обработки.
9.1.5. Право на ограничение обработки:
• Блокирование обработки на время проверки точности;
• Ограничение способов обработки;
• Приостановление передачи третьим лицам.
9.2. Процедура рассмотрения запросов:
9.2.1. Подача запроса:
• Запрос может быть подан лично, по почте или электронно;
• Запрос должен содержать сведения, указанные в ч. 3 ст. 14 Закона;
• При подаче запроса в электронной форме требуется подтверждение личности;
• Запрос может быть подан через представителя при наличии доверенности.
9.2.2. Сроки рассмотрения:
• Срок рассмотрения запроса составляет 30 дней с момента получения;
• Срок может быть продлен на 30 дней при большом объеме информации;
О продлении срока субъект уведомляется в течение 30 дней;
• Безотлагательно — в случаях, указанных в законе.
9.2.3. Содержание ответа:
• Подтверждение получения запроса;
• Запрашиваемая информация в доступной форме;
• Разъяснение порядка обжалования;
• Мотивированный отказ (при наличии оснований).
9.2.4. Способы предоставления ответа:
В письменной форме по почте;
В электронной форме (при согласии субъекта);
• Через личный кабинет на сайте;
• При личном обращении в офис Оператора.
9.3. Основания для отказа в удовлетворении запроса:
• Обработка необходима для защиты жизни, здоровья субъекта;
• Обработка необходима для выполнения функций государства;
• Персональные данные получены в рамках оперативно-розыскной деятельности;
• Обработка необходима для защиты прав и законных интересов третьих лиц;
• Исполнение судебного акта или иного акта государственного органа.
9.4. Плата за предоставление информации:
• Первый запрос в течение года обрабатывается бесплатно;
За повторные запросы может взиматься плата в размере расходов;
• Размер платы определяется в соответствии с законодательством;
• Субъект уведомляется о размере платы до предоставления информации.
10. Обеспечение безопасности персональных данных
------------------------------------------------
10.1. Правовые меры:
• Назначение ответственного за организацию обработки персональных данных;
• Принятие локальных актов по вопросам обработки персональных данных;
• Ознакомление работников с требованиями законодательства и локальными актами;
• Применение мер ответственности за нарушение требований законодательства.
10.2. Организационные меры:
• Определение перечня лиц, допущенных к обработке персональных данных;
• Установление правил доступа к персональным данным;
• Применение средств защиты, прошедших процедуру оценки соответствия;
• Оценка вреда, который может быть причинен субъектам персональных данных;
• Учет машинных носителей персональных данных;
• Обнаружение фактов несанкционированного доступа;
• Восстановление персональных данных.
10.3. Технические меры:
• Предотвращение несанкционированного доступа к персональным данным;
• Своевременное обнаружение фактов несанкционированного доступа;
• Предотвращение воздействия на технические средства обработки;
• Возможность незамедлительного восстановления персональных данных;
• Постоянный контроль за обеспечением уровня защищенности.
10.4. Конкретные технические решения:
• Использование сертифицированных средств защиты информации;
• Шифрование персональных данных при передаче и хранении;
• Применение межсетевых экранов и систем обнаружения вторжений;
• Резервное копирование и обеспечение отказоустойчивости;
• Аудит и мониторинг доступа к информационным системам;
• Антивирусная защита и обновление программного обеспечения.
10.5. Контроль доступа:
• Идентификация и аутентификация пользователей;
• Разграничение доступа по ролям и полномочиям;
• Протоколирование действий пользователей;
• Регулярный пересмотр прав доступа;
• Блокирование учетных записей при увольнении сотрудников.
11. Сведения о реализуемых требованиях к защите персональных данных
------------------------------------------------------------
11.1. Уровни защищенности:
Оператор определяет уровни защищенности персональных данных в соответствии с Постановлением Правительства РФ от 01.11.2012 № 1119 и обеспечивает соответствующие им требования безопасности.
11.2. 1-й уровень защищенности (общедоступные персональные данные):
• Базовые средства защиты от несанкционированного доступа;
• Парольная защита доступа к информационным системам;
• Антивирусная защита рабочих станций и серверов.
11.3. 2-й уровень защищенности (иные персональные данные, кроме специальных и биометрических):
• Идентификация и аутентификация субъектов доступа;
• Управление доступом субъектов доступа к ресурсам;
• Ограничение программной среды;
• Защита машинных носителей информации;
• Регистрация событий безопасности;
• Антивирусная защита;
• Обнаружение вторжений;
• Контроль целостности информационной системы и информации.
11.4. 3-й уровень защищенности (специальные категории персональных данных):
Дополнительно к требованиям 2-го уровня:
• Обеспечение целостности информационной системы и информации;
• Обеспечение доступности информационной системы и информации;
• Защита технических средств;
• Защита информационной системы, ее средств, систем связи и передачи данных.
11.5. 4-й уровень защищенности (биометрические персональные данные):
Дополнительно к требованиям 3-го уровня:
• Предотвращение внедрения в информационную систему программных закладок;
• Анализ защищенности информационной системы.
12. Заключительные положения
----------------------------
12.1. Внесение изменений в Политику:
• Политика может изменяться в связи с изменениями в законодательстве;
• Существенные изменения доводятся до сведения субъектов персональных данных;
• Действующая версия Политики размещается на официальном сайте;
• Дата последнего обновления указывается в Политике.
12.2. Жалобы и обращения:
• Субъекты персональных данных могут обратиться к Оператору по вопросам обработки;
• Жалобы рассматриваются в установленном законом порядке;
• При неурегулировании разногласий возможно обращение в Роскомнадзор или суд.
12.3. Применимое право:
• Политика регулируется законодательством Российской Федерации;
• Споры рассматриваются в российских судах;
• При противоречии переводов приоритет имеет русскоязычная версия.
12.4. Контактная информация для обращений:
• Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н
• Электронная почта: company@bitforcefoundation.ru
12.5. Вступление в силу:
• Настоящая Политика вступает в силу с момента ее утверждения и размещения на официальном сайте Оператора.

235
publichnaya-oferta.txt Normal file
View File

@@ -0,0 +1,235 @@
ПУБЛИЧНЫЙ ДОГОВОР ОФЕРТЫ
========================
ООО БИТФОРС
Агентский договор
-----------------
Настоящая оферта на заключение агентского договора
(далее Оферта, Договор) является публичным предложением
Общества с ограниченной ответственностью «БИТФОРС», заключить договор на условиях и в порядке, определенных настоящей Офертой.
Акцепт оферты производится в соответствии с пунктом 2 статьи 437 Гражданского кодекса Российской Федерации и равносилен заключению агентского договора в письменной форме.
Основные понятия и определения действующего договора
----------------------------------------------------
Агент юридическое лицо или индивидуальный предприниматель, зарегистрированный на территории Российской Федерации, в установленном действующим законодательством порядке.
Принципал сторона агентского договора, по поручению которой агент осуществляет юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала.
Агентский договор соглашение, по которому агент обязуется за вознаграждение совершать по поручению принципала юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала в соответствии с п. 1 ст. 1005 Гражданского Кодекса Российской Федерации.
Личный кабинета Агента ресурс, размещенный на сайте Принципала, предназначенный для взаимодействия Агента и Принципала.
Отчетный период период для взаиморасчетов с Агентом, равный
одному календарному кварталу с даты активации любой из услуг, предоставляемой Принципалу.
Отчет о сумме начислений (Отчет) отчет, формируемый в Личном кабинете Агента на основании данных систем учета Принципала.
Оферта (Договор) настоящий документ, который отражает предложение и намерение ООО «БИТФОРС» считать заключенным договор с лицом, которым будет принято предложение на условиях, изложенных ниже.
1. Акцепт оферты и заключение агентского договора
-------------------------------------------------
1.1. Акцепт настоящей Оферты и заключение Агентского договора осуществляется Принципалом в процессе регистрации в Личном кабинете Принципала (на сайте Агента), при прочтении текста настоящей Оферты, путем проставления специальной отметки (галочки) напротив фразы
«Я ознакомился с Офертой и принимаю ее условия» и нажатия кнопки «Подписать».
1.2. Особый порядок принятия условий Оферты путем проставления специальной отметки (галочки) определяется интерфейсом Личного кабинета Принципала. Принципал не может зарегистрироваться в Личном кабинете и получить к нему доступ без подтверждения принятия условий Оферты.
1.3. Принимая Оферту, Принципал подтверждает, что прочел и полностью согласен с документами, размещенными на сайте в разделе, предназначенном для Принципала, которые являются неотъемлемой частью настоящей Оферты (Договора) и обязательны для исполнения Сторонами.
2. Общие положения
------------------
2.1. Публикуемые на сайте Агента (ссылка на сайт) документы
(формы, требования, правила и т.п.), устанавливающие порядок и условия выполнения действий, предусмотренных настоящим Договором, являются неотъемлемой частью настоящего Договора и обязательны для исполнения Сторонами. Принципал обязан использовать формы документов, утвержденных Агентом, и не вправе вносить в них какие-либо изменения или дополнения.
2.2. Агент обязуется уведомлять Принципала обо всех изменениях в документах, связанных с исполнением настоящего Договора, путем направления электронных сообщений (через Личный кабинет или на электронную почту Принципала) или размещением уведомлений об изменениях на сайте Агентов в разделе, предназначенном для размещения объявлений. Такие сообщения и уведомления приравниваются к сообщениям и уведомлениям, исполненным в простой письменной форме, направляемым на почтовые адреса Принципала.
2.3. К правам и обязанностям сторон, возникшим на основании настоящего Договора, применяются положения действующей редакции Договора и Приложений, опубликованных на сайте Агента, если иное не установлено Договором.
3. Предмет договора
-------------------
3.1. По настоящему Договору Принципал поручает, а Агент принимает на себя обязательство совершать от имени и за счет Принципала указанные в п. 3.2 настоящего Договора действия, а Принципал обязуется выплатить Агенту вознаграждение за совершенные действия.
3.2. По настоящему Договору Агент совершает следующие действия:
Консультирование Принципала об услугах Агента, включая, помимо прочего, порядок активации и оказания услуг, работу в Личном кабинете Принципала и иные дополнительные услуги, оказываемые Агентом;
Совершение сделок и иных юридических действий Агентом от своего имени, но за счёт Принципала.
3.3. Настоящий Договор действует на территории Российской Федерации и иного иностранного государства.
3.4. Права и обязанности по сделкам, совершенным Агентом во исполнение настоящего Договора, возникают непосредственно у Принципала.
3.5. Агент гарантирует отсутствие договорных и иных отношений с лицами, которые могли бы оказать влияние на исполнение настоящего Договора. Агент гарантирует свою независимость и объективность в ходе исполнения настоящего Договора.
4. Права и обязанности сторон
-----------------------------
4.1. Агент обязуется:
4.1.1. Совершать действия, составляющие предмет настоящего Договора, в соответствии с законными интересами Принципала.
4.1.2. Сообщать Принципалу по его требованию все сведения о ходе исполнения настоящего Договора.
4.1.3. Передавать Принципалу в течение 7 (семи) рабочих дней имущество, полученное по сделкам, совершенным во исполнение настоящего Договора.
4.1.4. Не позднее последнего дня месяца, следующего за отчетным, представлять Принципалу отчет об исполнении поручения с приложением доказательств расходов, произведенных Агентом за счет Принципала.
4.1.5. Выполнять другие обязанности, которые в соответствии с настоящим Договором или законом возлагаются на Агента.
4.2. Агент несет ответственность за сохранность документов и персональных данных, переданных ему Принципалом для исполнения настоящего Договора.
4.3. Агент вправе:
4.3.1. Отступить от указаний Принципала, если по обстоятельствам дела это необходимо в интересах Принципала и Агент не мог предварительно запросить Принципала либо не получил в течение 3 (трех) рабочих дней ответа на свой запрос. Агент обязан уведомить Принципала о допущенных отступлениях, как только уведомление станет возможным.
4.3.2. Удержать причитающиеся ему по настоящему Договору суммы вознаграждения из всех сумм, поступивших к нему за счет Принципала.
4.3.3. Агент вправе заключить субагентский договор с другим лицом. В случае заключения субагентского договора ответственным за действия субагента перед Принципалом остается Агент.
4.4. Принципал обязан:
4.4.1. Без промедления принять отчет Агента, все предоставленные им документы и все исполненное им в соответствии с Договором. Принципал, имеющий возражения по отчету Агента, должен сообщить о них Агенту в течение 7 (семи) рабочих дней со дня получения отчета. В противном случае отчет считается принятым Принципалом.
4.4.2. Обеспечить Агента документами и материалами, необходимыми для выполнения настоящего Договора.
4.4.3. Возместить Агенту понесенные в связи с исполнением настоящего Договора расходы.
4.4.4. Выплатить Агенту обусловленное настоящим Договором агентское вознаграждение.
4.5. Принципал вправе:
4.5.1. Давать Агенту рекомендации об исполнении настоящего Договора. Указания Принципала должны быть правомерными, осуществимыми и конкретными.
4.5.2. Получать от Агента сведения о ходе выполнения поручения по требованию.
4.5.3. Требовать от Агента представления отчета о проделанной работе во исполнение настоящего Договора.
5. Агентское вознаграждение и порядок оплаты
--------------------------------------------
5.1. Сумма вознаграждения Агента по настоящему Договору составляет
- 8% от 5 000 до 30 000 рублей (вычет процента производится с учетом всех операционных расходов, необходимых для исполнения поручения от своего имени, но за счет Принципала Агентом).
- 6% от 30 000 до 100 000 рублей (вычет процента производится с учетом всех операционных расходов, необходимых для исполнения поручения от своего имени, но за счет Принципала Агентом).
- 4% от 100 000 до 600 000 рублей (вычет процента производится с учетом всех операционных расходов, необходимых для исполнения поручения от своего имени, но за счет Принципала Агентом).
5.2. Вознаграждение выплачивается Агенту в следующем порядке и сроки:
- Принципал выплачивает Агенту вознаграждение с момента подписания настоящего Договора об исполнении поручения Агентом от своего имени, но за счет Принципала.
5.3. Принципал возмещает следующие расходы Агента:
5.3.1. Расходы на оплату банковских услуг и иных комиссий в сумме не более 30 000 рублей.
5.4. Расходы, указанные в п. 5.3 настоящего Договора, возмещаются Принципалом в следующем порядке:
- Возмещение расходов Агенту осуществляется в момент подписания настоящего Договора об исполнении поручения Агентом от своего имени, но за счет Принципала.
5.5. Безналичные расчеты по настоящему Договору производятся Сторонами путем перечисления денежных средств на расчетный счет Стороны по реквизитам, указанным в настоящем Договоре. Размер агентского вознаграждения устанавливается договором между сторонами и выплачивается Агенту за совершение юридических и (или) фактических действий в интересах Принципала.
6. Ответственность сторон
-------------------------
6.1. В случае нарушения Агентом срока, установленного п. 4.1.3 настоящего Договора для передачи Принципалу полученного по настоящему Договору, Принципал вправе предъявить Агенту требование об уплате неустойки в размере 0,1% от непереданной денежной суммы (либо от стоимости непереданного имущества) за каждый день просрочки.
6.2. В случае нарушения Принципалом срока уплаты вознаграждения, установленного п. 5.2 настоящего Договора, или срока возмещения расходов, установленного п. 5.4 настоящего Договора, Агент вправе предъявить Принципалу требование об уплате неустойки в размере 0,1% от не уплаченной в срок суммы за каждый день просрочки.
6.3. Ответственность Сторон за неисполнение или ненадлежащее исполнение иных обязательств по настоящему Договору определяется в соответствии с нормами действующего законодательства Российской Федерации.
7. Форс-мажор
-------------
7.1. Стороны освобождаются от ответственности за частичное или полное неисполнение обязательств по настоящему Договору, если это неисполнение явилось следствием возникших после заключения настоящего Договора обстоятельств непреодолимой силы, которые Стороны не могли предвидеть или предотвратить.
7.2. При наступлении обстоятельств, указанных в п. 7.1 настоящего Договора, каждая Сторона должна без промедления известить о них в письменном виде другую Сторону. Извещение должно содержать данные о характере обстоятельств, а также официальные документы, удостоверяющие наличие этих обстоятельств и, по возможности, дающие оценку их влияния на исполнение Стороной своих обязательств по настоящему Договору.
7.3. Если Сторона не направит или несвоевременно направит извещение, предусмотренное в п. 7.2 настоящего Договора, то она обязана возместить второй Стороне понесенные ею убытки.
7.4. В случаях наступления обстоятельств, предусмотренных в п. 7.1 настоящего Договора, срок выполнения Стороной обязательств по настоящему Договору отодвигается соразмерно времени, в течение которого действуют эти обстоятельства и их последствия.
7.5. Если обстоятельства, указанные в п. 7.1 настоящего Договора, и их последствия продолжают действовать более трех недель:
- Стороны проводят дополнительные переговоры для выявления приемлемых альтернативных способов исполнения настоящего Договора.
- Сторона, не затронутая ее действием, вправе расторгнуть Договор в одностороннем порядке, направив другой Стороне соответствующее извещение и не возмещая каких-либо убытков, вызванных расторжением Договора.
8. Конфиденциальность
---------------------
8.1. Стороны принимают все необходимые меры для того, чтобы их сотрудники, агенты, правопреемники без предварительного согласия другой Стороны не информировали третьих лиц о конфиденциальной информации и персональных данных Сторон настоящего Договора.
9. Изменение и прекращение договора
-----------------------------------
9.1. Настоящий договор вступает в силу с момента его подписания и действует до момента исполнения сторонами своих обязательств по настоящему договору.
9.2. Настоящий Договор может быть изменен или прекращен по письменному соглашению Сторон, а также в других случаях, предусмотренных законодательством Российской Федерации.
9.3. Принципал вправе в любое время отказаться от исполнения настоящего Договора путем направления письменного уведомления Агенту за 3 (три) рабочих дня.
9.4. В случае отказа от настоящего Договора Принципал обязан незамедлительно после направления уведомления Агенту распорядиться своим имуществом, находящимся в ведении Агента, и не позднее 7 (семи) рабочих дней произвести выплату причитающегося Агенту вознаграждения за действия, совершенные им до прекращения Договора.
9.5. Агент вправе отказаться от исполнения настоящего Договора путем направления письменного уведомления Принципалу во всякое время.
9.6. Агент обязан принять меры, необходимые для обеспечения сохранности имущества Принципала. Принципал должен незамедлительно распорядиться своим находящимся в ведении Агента имуществом.
9.7. Агент, отказавшийся от настоящего Договора, сохраняет право на вознаграждение за действия, выполненные им до прекращения Договора.
10. Заключительные положения
----------------------------
10.1. Ни одна из сторон не вправе передавать свои права и обязанности по настоящему договору третьим лицам без согласия другой стороны.
10.2. Если иное не предусмотрено Договором, извещения, уведомления, требования и иные юридически значимые сообщения (далее - сообщения) Стороны могут направлять по факсу, электронной почте или другим способом связи при условии, что он позволяет достоверно установить, от кого исходило сообщение и кому оно адресовано.
10.3. Сообщения влекут гражданско-правовые последствия для Стороны, которой направлены, с момента их доставки указанной Стороне или ее представителю. Такие последствия возникают и в случае, когда сообщение не было вручено адресату по зависящим от него обстоятельствам
(п. 1 ст. 165.1 Гражданский Кодекс Российской Федерации).
10.4. Во всем остальном, что не предусмотрено настоящим Договором, Стороны руководствуются действующим законодательством Российской Федерации.
10.5. Споры, вытекающие из настоящего Договора, разрешаются в досудебном порядке. При неурегулировании возникших разногласий спор разрешается в Арбитражном суде г. Санкт–Петербурга и Ленинградской области с обязательным соблюдением претензионного порядка. Срок ответа на претензию составляет 14 (четырнадцать) рабочих дней.
Реквизиты сторон
----------------
Общество с ограниченной ответственностью «БИТФОРС»
196246, г. Санкт-Петербург, Московский р-н, Московское шоссе, д.25к1 литера в, помещ. 3-Н
ИНН / КПП 9810001062 / 781001001
ОГРН 1257800060990
ОКПО / ОКАТО / ОКТМО 68342261 / 40284000000 / 40377000000
Руководитель: Кленин Михаил Васильевич
Электронная почта: company@bitforcefoundation.ru
Наименование банка: ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"
Корреспондентский счет 30101810600000000786
БИК 044030786
Расчетный счет 40702810632250004861

View File

@@ -0,0 +1,4 @@
Реестр операторов персональных данных (Роскомнадзор)
ООО «БИТФОРС»
https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn=

View File

@@ -0,0 +1,337 @@
СОГЛАСИЕ НА ОБРАБОТКУ ПЕРСОНАЛЬНЫХ ДАННЫХ
=========================================
ООО «БИТФОРС»
Преамбула
---------
Я, субъект персональных данных, действуя своей волей и в своем интересе, в соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных» (далее — Закон), предоставляю ООО «БИТФОРС» (далее — Оператор, Общество) согласие на обработку моих персональных данных на условиях и для целей, определенных настоящим Согласием.
1. Сведения об операторе
------------------------
Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»
ИНН: 9810001062
ОГРН: 1257800060990
Реестр ОПД: Посмотреть в реестре Роскомнадзора (https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn=)
Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н
Контактная информация:
• Электронная почта: company@bitforcefoundation.ru
Веб-сайт: https://bitforce-foundation.ru
2. Правовые основания обработки
-------------------------------
2.1. Настоящее согласие предоставляется на основании пункта 1 части 1 статьи 6 Федерального закона «О персональных данных» и является правовым основанием для обработки персональных данных Оператором.
2.2. Согласие дается добровольно, своей волей и в своих интересах.
2.3. Субъект персональных данных понимает последствия предоставления согласия, включая возможные риски, связанные с обработкой персональных данных.
3. Цели обработки персональных данных
-------------------------------------
Согласие предоставляется для обработки персональных данных в следующих целях:
3.1. Основные цели:
• Регистрация и ведение учетной записи на веб-сайте https://bitforce-foundation.ru и в мобильном приложении;
• Идентификация и верификация личности в соответствии с требованиями законодательства Российской Федерации;
• Предоставление услуг по обмену криптовалют и электронных денежных средств;
• Проведение финансовых операций, переводов и расчетов;
• Ведение учета и истории операций.
3.2. Дополнительные цели:
• Обеспечение безопасности операций и предотвращение мошенничества;
• Выполнение требований по противодействию легализации доходов, полученных преступным путем, и финансированию терроризма;
• Соблюдение требований валютного, налогового и иного применимого законодательства;
• Предоставление технической поддержки и клиентского сервиса;
• Рассылка уведомлений о состоянии операций и изменениях в условиях предоставления услуг.
3.3. Маркетинговые цели (при дополнительном согласии):
• Направление информационных и рекламных материалов;
• Проведение маркетинговых исследований и опросов;
• Персонализация предложений и услуг;
• Анализ предпочтений и поведения для улучшения сервисов.
3.4. Аналитические цели:
• Анализ использования веб-сайта и мобильного приложения;
• Улучшение качества предоставляемых услуг;
• Разработка новых продуктов и сервисов;
• Создание статистических отчетов в обезличенном виде.
4. Перечень персональных данных, на обработку которых дается согласие
------------------------------------------------------------
4.1. Идентификационные данные:
• Фамилия, имя, отчество;
• Дата рождения;
• Место рождения;
• Гражданство;
• Пол.
4.2. Документы, удостоверяющие личность:
• Серия и номер паспорта гражданина Российской Федерации;
• Дата выдачи и код подразделения;
• Наименование органа, выдавшего документ;
• Адрес регистрации по месту жительства;
• Цифровые копии (сканы) документов.
4.3. Контактная информация:
• Номера телефонов (мобильный, домашний, рабочий);
• Адреса электронной почты;
• Почтовые адреса (фактического проживания, для корреспонденции);
• Данные мессенджеров и социальных сетей (при предоставлении).
4.4. Финансовая информация:
• Номера банковских счетов и реквизиты банковских карт;
• Реквизиты криптовалютных кошельков и криптовалютных адресов;
• Информация о доходах и источниках происхождения денежных средств;
• История финансовых операций и транзакций;
• Данные о налоговом статусе и резидентстве.
4.5. Техническая информация:
• IP-адреса устройств, с которых осуществляется доступ к сервисам;
• Информация о браузере, операционной системе и устройстве;
• Файлы cookie и данные локального хранилища;
• Логи действий и история использования сервисов;
• Геолокационные данные (при включенной функции).
4.6. Дополнительная информация:
• Фотографии для процедур верификации;
• Видеозаписи процедур видеоидентификации;
• Биометрические данные (при использовании соответствующих технологий);
• Информация о семейном положении и близких родственниках (при необходимости);
• Сведения о профессиональной деятельности и должности;
• Любая иная информация, предоставленная субъектом добровольно.
4.7. Специальные категории персональных данных:
При необходимости и при наличии отдельного письменного согласия:
• Биометрические персональные данные;
• Данные о состоянии здоровья (при оформлении страхования);
• Данные о судимости (при проверках безопасности).
5. Перечень действий с персональными данными
--------------------------------------------
Согласие распространяется на следующие действия (операции) с персональными данными:
5.1. Действия по получению и первичной обработке:
Сбор персональных данных;
• Запись на материальные и электронные носители;
• Первичная систематизация и структурирование;
• Проверка достоверности и полноты данных.
5.2. Действия по хранению и систематизации:
• Накопление персональных данных в базах данных;
• Систематизация и каталогизация;
• Создание резервных копий;
• Архивирование данных.
5.3. Действия по использованию и анализу:
• Извлечение персональных данных из баз данных;
• Использование для достижения заявленных целей;
• Анализ и обработка для получения аналитической информации;
• Сопоставление с данными из других источников.
5.4. Действия по изменению и актуализации:
• Уточнение (обновление, изменение) персональных данных;
• Дополнение новой информацией;
• Исправление выявленных неточностей;
• Актуализация устаревших данных.
5.5. Действия по передаче:
• Передача персональных данных третьим лицам;
• Предоставление доступа уполномоченным лицам;
5.6. Действия по обезличиванию и уничтожению:
• Обезличивание персональных данных;
• Блокирование доступа к персональным данным;
• Удаление персональных данных;
• Уничтожение носителей персональных данных.
5.7. Автоматизированная обработка:
• Автоматический сбор данных через веб-сайт и приложения;
• Автоматизированный анализ и принятие решений;
• Машинное обучение и использование алгоритмов;
• Профилирование и сегментация.
6. Лица, которым могут быть переданы персональные данные
--------------------------------------------------------
6.1. Сотрудники Оператора:
• Уполномоченные сотрудники, непосредственно участвующие в обработке персональных данных;
• Сотрудники службы безопасности и комплаенса;
• Сотрудники технической поддержки;
• Руководящий состав в рамках их полномочий.
6.2. Государственные и муниципальные органы:
• Федеральная служба по финансовому мониторингу (Росфинмониторинг);
• Федеральная налоговая служба;
• Правоохранительные органы (при наличии законных требований);
• Суды и органы исполнения судебных решений;
• Иные государственные органы в рамках их компетенции.
6.3. Партнеры и контрагенты:
• Банки;
• Платежные системы;
• Операторы электронных денежных средств;
• Поставщики технологических решений;
• Аудиторские и консалтинговые организации.
6.4. Третьи лица для специальных целей:
• Службы доставки и курьерские службы;
• Телекоммуникационные операторы;
• Маркетинговые агентства (при согласии на маркетинг);
• Сервисы аналитики и веб-метрики;
• Облачные провайдеры и хостинг-провайдеры;
• Архивные организации.
6.5. Условия передачи:
• Передача осуществляется только для целей, указанных в настоящем согласии;
• Получатели обязуются обеспечить конфиденциальность и безопасность данных;
• Заключаются соответствующие соглашения о защите персональных данных.
7. Сроки обработки персональных данных
--------------------------------------
7.1. Общие принципы определения сроков:
• Персональные данные обрабатываются в течение времени, необходимого для достижения целей обработки;
• Сроки определяются требованиями законодательства и характером отношений с Оператором;
• После достижения целей обработки данные подлежат уничтожению или обезличиванию.
7.2. Конкретные сроки обработки:
Данные активных клиентов:
В течение всего периода действия отношений с Оператором;
• Плюс 5 лет после прекращения отношений (в соответствии с требованиями валютного законодательства);
• Плюс дополнительные сроки архивного хранения согласно законодательству.
Данные для идентификации и верификации:
• 5 лет с момента прекращения отношений с клиентом;
• 5 лет с даты проведения последней операции;
• До истечения сроков исковой давности по спорным операциям.
Финансовая информация и история операций:
• 5 лет с даты совершения операции (требования по ПОД/ФТ);
• 5 лет для налогового учета;
• До завершения всех расследований и судебных процедур.
Маркетинговые данные:
• До отзыва согласия на маркетинговые коммуникации;
Не более 3 лет с момента последнего взаимодействия;
• Немедленное удаление при отказе от рассылок.
Техническая информация (логи, IP-адреса):
• 1 год для обеспечения информационной безопасности;
• 6 месяцев для технических логов;
• Постоянно в обезличенном виде для статистики.
7.3. Досрочное прекращение обработки:
• При отзыве согласия субъектом персональных данных;
• При выявлении незаконности получения или обработки;
• По требованию уполномоченных органов;
• При ликвидации Оператора.
8. Права субъекта персональных данных
-------------------------------------
8.1. Основные права:
Право на информацию:
• Получение подтверждения факта обработки персональных данных;
• Получение информации о целях, правовых основаниях и способах обработки;
• Получение сведений о сроках обработки и составе данных;
• Информация о третьих лицах, которым передаются данные.
Право на доступ:
• Получение копий обрабатываемых персональных данных;
• Ознакомление с историей обработки и изменений;
• Получение информации об источниках персональных данных;
• Доступ к автоматизированным решениям, принятым на основе данных.
Право на исправление:
• Требование исправления неточных или неполных данных;
• Дополнение недостающей информации;
• Актуализация устаревших данных;
• Получение подтверждения о внесенных изменениях.
Право на удаление ("право на забвение"):
• Требование удаления персональных данных при наличии оснований;
• Удаление данных после отзыва согласия;
• Удаление при прекращении правовых оснований для обработки;
• Получение подтверждения об удалении.
Право на ограничение обработки:
• Требование блокирования обработки на время проверки точности данных;
• Ограничение способов обработки;
• Приостановление передачи третьим лицам;
• Сохранение данных без их активного использования.
8.2. Право на отзыв согласия:
• Согласие может быть отозвано в любое время;
• Отзыв не влияет на законность обработки до момента отзыва;
• Отзыв оформляется в письменной форме;
• После отзыва обработка прекращается в разумные сроки.
8.3. Право на обжалование:
• Обращение к Оператору с жалобами на действия по обработке данных;
• Обращение в Роскомнадзор или его территориальные органы;
• Обращение в суд для защиты нарушенных прав;
• Требование возмещения морального и материального вреда.
8.4. Порядок реализации прав:
• Обращения направляются на адрес: company@bitforcefoundation.ru;
• Обращения рассматриваются в течение 30 дней;
• При необходимости срок может быть продлен на 30 дней;
• Ответ предоставляется в письменной или электронной форме по выбору субъекта.
9. Заключительные положения
---------------------------
9.1. Действие согласия:
• Согласие действует с момента его предоставления;
• Согласие действует до его отзыва или до достижения целей обработки;
• Согласие может быть изменено по взаимному соглашению сторон;
• При существенных изменениях целей требуется новое согласие.
9.2. Форма предоставления согласия:
• Согласие может быть предоставлено в письменной форме;
• Согласие может быть предоставлено в электронной форме;
• Согласие может выражаться путем совершения конклюдентных действий;
• Согласие фиксируется и сохраняется Оператором.
9.3. Последствия непредоставления согласия:
• Отказ в предоставлении согласия может повлечь невозможность регистрации;
• Отказ может ограничить доступ к отдельным услугам;
• Отказ в согласии на маркетинг не влияет на основные услуги;
• Субъект вправе предоставить частичное согласие.
9.4. Контактная информация:
Для реализации прав и направления обращений:
• Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н
• Электронная почта: company@bitforcefoundation.ru
• Ответственное лицо: Кленин Михаил Васильевич
• Официальный сайт: https://bitforce-foundation.ru
9.5. Подтверждение понимания:
Предоставляя настоящее согласие, я подтверждаю, что:
• Ознакомлен с содержанием согласия и понимаю его значение;
• Понимаю цели и способы обработки моих персональных данных;
• Знаю о своих правах и способах их реализации;
• Согласие предоставляется добровольно и осознанно;
• Имею возможность отозвать согласие в любое время.

View File

@@ -1,175 +0,0 @@
<!DOCTYPE html><html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ЭКСА — Сид Фраза</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0A0B2E;
color: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* Navbar */
.nav{display:flex;align-items:center;justify-content:space-between;padding:0 32px;height:60px;border-bottom:1px solid rgba(255,255,255,0.06);flex-shrink:0}
.nav-logo{display:flex;align-items:center}
.nav-logo img{height:32px}
.ticker{display:flex;gap:24px;font-size:13px;font-family:var(--mono)}
.tick{display:flex;align-items:center;gap:6px;color:#B5B0CC}
.tick b{color:#fff}
.tick .up{color:#00C48C}.tick .dn{color:#FF4D4D}
.nav-account{display:flex;align-items:center;gap:10px}
.avatar{width:34px;height:34px;border-radius:50%;background:#3D2A8E}
.nav-account span{color:#B5B0CC;font-size:14px;font-weight:500}
/* Content */
.content {
max-width: 960px;
margin: 0 auto;
padding: 40px 32px 60px;
}
/* Title row */
.title-row {
display: flex; align-items: flex-start; justify-content: space-between;
}
.title-row h1 {
font-size: 20px; font-weight: 700; letter-spacing: 0.04em;
}
.title-buttons {
display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
}
.btn-outline {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
border-radius: 10px;
width: 160px;
padding: 10px 0;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
text-align: center;
}
.btn-outline:hover {
background: rgba(255,255,255,0.12);
border-color: rgba(255,255,255,0.18);
}
/* Subtitle */
.subtitle {
margin-top: 12px;
font-size: 12px;
color: #B5B0CC;
font-variant: all-small-caps;
letter-spacing: 0.08em;
}
.subtitle .countdown { color: #4A6DFF; font-weight: 700; }
/* Grid */
.seed-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 32px;
}
.seed-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
height: 52px;
display: flex; align-items: center;
padding: 0 18px;
gap: 10px;
transition: border-color 0.25s, box-shadow 0.25s;
cursor: default;
user-select: none;
}
.seed-card:hover {
border-color: rgba(74,109,255,0.4);
box-shadow: 0 0 12px rgba(74,109,255,0.15);
}
.seed-num {
color: #B5B0CC;
font-size: 13px;
min-width: 22px;
flex-shrink: 0;
}
.seed-word {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 700;
color: #fff;
}
/* Warning */
.warning {
margin-top: 32px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.warning p {
max-width: 480px;
font-size: 13px;
color: #B5B0CC;
line-height: 1.6;
}
.warning .icon { color: #FF4D4D; font-size: 18px; margin-bottom: 8px; }
</style>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>:root{--mono:'JetBrains Mono',monospace}</style></head>
<body data-cc-id="cc-4" style="cursor: crosshair; font-family: Manrope;">
<nav class="nav">
<div class="nav-logo">
<img src="logo-full-white.png" alt="ЭКСА">
</div>
<div class="ticker">
<div class="tick"><b>BTC</b> $66,916.00 <span class="up">+0.12%</span></div>
<div class="tick"><b>ETH</b> $2,053.97 <span class="dn">0.12%</span></div>
<div class="tick"><b>SOL</b> $163.84 <span class="dn">1.57%</span></div>
</div>
<div class="nav-account">
<div class="avatar"></div>
<span>Account 1</span>
</div>
</nav>
<main class="content" style="font-family: Manrope" data-cc-id="cc-5">
<div class="title-row" style="height: 70px; width: 896px" data-cc-id="cc-16">
<h1 style="width: 250px; font-size: 32px; font-family: Manrope;" data-cc-id="cc-17">СИД ФРАЗА</h1>
<div class="title-buttons">
<button class="btn-outline">СКРЫТЬ</button>
<button class="btn-outline" style="font-size: 13px">КОПИРОВАТЬ</button>
</div>
</div>
<div class="subtitle" style="font-size: 14px" data-cc-id="cc-15">АВТОМАТИЧЕСКОЕ СКРЫТИЕ ЧЕРЕЗ <span class="countdown" id="countdown">14</span>С</div>
<div class="seed-grid" id="seedGrid" data-cc-id="cc-9"><div class="seed-card"><span class="seed-num">1.</span><span class="seed-word">egg</span></div><div class="seed-card" data-cc-id="cc-13"><span class="seed-num">2.</span><span class="seed-word" data-cc-id="cc-14">phone</span></div><div class="seed-card"><span class="seed-num">3.</span><span class="seed-word">long</span></div><div class="seed-card"><span class="seed-num">4.</span><span class="seed-word">vibe</span></div><div class="seed-card" data-cc-id="cc-11"><span class="seed-num">5.</span><span class="seed-word" data-cc-id="cc-12">potato</span></div><div class="seed-card"><span class="seed-num">6.</span><span class="seed-word">soup</span></div><div class="seed-card" data-cc-id="cc-7"><span class="seed-num">7.</span><span class="seed-word" data-cc-id="cc-8">skirt</span></div><div class="seed-card" data-cc-id="cc-10"><span class="seed-num">8.</span><span class="seed-word">black</span></div><div class="seed-card"><span class="seed-num">9.</span><span class="seed-word">phase</span></div><div class="seed-card" data-cc-id="cc-6"><span class="seed-num">10.</span><span class="seed-word">word</span></div><div class="seed-card"><span class="seed-num">11.</span><span class="seed-word">num</span></div><div class="seed-card"><span class="seed-num">12.</span><span class="seed-word">cucumber</span></div></div>
<div class="warning" style="justify-content: center; flex-direction: row; align-items: flex-start">
<div class="icon" style="padding: 16px">⚠️</div>
<p>Никогда не передавайте сид-фразу третьим лицам. Тот, кто знает фразу — владеет кошельком.</p>
</div>
</main>
<script>
let sec = 52;
const el = document.getElementById('countdown');
setInterval(() => {
if (sec > 0) { sec--; el.textContent = sec; }
}, 1000);
</script>
</body></html>

View File

@@ -2,12 +2,25 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { HomePage } from '@pages/home'
import { WalletPage } from '@pages/wallet'
import { SwapPage } from '@pages/swap'
import { BridgePage } from '@pages/bridge'
import { ProfilePage } from '@pages/profile'
import { LoginPage } from '@pages/login'
import { RegisterPage } from '@pages/register'
import { RegisterTestPage } from '@pages/register-test'
import { ConverterTestPage } from '@pages/converter-test'
import { ConverterPage } from '@pages/converter'
import { SeedPhrasePage } from '@pages/seed-phrase'
import { KycPage } from '@pages/kyc'
import { RestorePasswordPage } from '@pages/restore-password'
import { PublichnayaOfertaPage } from '@pages/publichnaya-oferta'
import { PolitikaPage } from '@pages/politika-personalnyh-dannyh'
import { PolitikaCookiePage } from '@pages/politika-cookie'
import { SoglasiePage } from '@pages/soglasie-personalnyh-dannyh'
import { ReestryPage } from '@pages/reestr-pd-rkn'
import { TransactionsPage } from '@pages/transactions'
import { AdminPage } from '@pages/admin'
import { AdminOrganizationPage } from '@pages/admin-organization'
import { WalletLayout } from '@widgets/wallet-layout'
import { ROUTES } from '@shared/config/routes'
import { ScrollToTop } from './ScrollToTop'
import { ProtectedRoute } from './ProtectedRoute'
@@ -19,21 +32,41 @@ export function RouterProvider() {
<ScrollToTop />
<Routes>
<Route path={ROUTES.HOME} element={<HomePage />} />
<Route path={ROUTES.PUBLICHNAYA_OFERTA} element={<PublichnayaOfertaPage />} />
<Route path={ROUTES.POLITIKA_PERSONALNYH_DANNYH} element={<PolitikaPage />} />
<Route path={ROUTES.POLITIKA_COOKIE} element={<PolitikaCookiePage />} />
<Route path={ROUTES.SOGLASIE_PERSONALNYH_DANNYH} element={<SoglasiePage />} />
<Route path={ROUTES.REESTR_PD_RKN} element={<ReestryPage />} />
<Route path={ROUTES.REGISTER_TEST} element={<RegisterTestPage />} />
<Route path={ROUTES.CONVERTER_TEST} element={<ConverterTestPage />} />
{/* Admin panel — own auth gate, independent of the user auth system */}
<Route path={ROUTES.ADMIN} element={<AdminPage />} />
<Route path={ROUTES.ADMIN_ORGANIZATION} element={<AdminOrganizationPage />} />
<Route element={<GuestRoute />}>
<Route path={ROUTES.LOGIN} element={<LoginPage />} />
<Route path={ROUTES.REGISTER} element={<RegisterPage />} />
<Route path={ROUTES.RESTORE_PASSWORD} element={<RestorePasswordPage />} />
</Route>
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
<Route path={ROUTES.WALLET} element={<WalletPage />} />
<Route element={<ProtectedRoute />}>
<Route element={<WalletLayout footer center />}>
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
</Route>
<Route element={<WalletLayout footer />}>
<Route path={ROUTES.SWAP} element={<SwapPage />} />
<Route path={ROUTES.BRIDGE} element={<BridgePage />} />
<Route path={ROUTES.TRANSACTIONS} element={<TransactionsPage />} />
</Route>
<Route path={ROUTES.WALLET} element={<WalletPage />} />
<Route path={ROUTES.WALLET_CHAIN} element={<WalletPage />} />
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
<Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} />
</Route>
<Route path={ROUTES.KYC} element={<KycPage />} />
</Route>
</Routes>
</BrowserRouter>
)

View File

@@ -5,8 +5,28 @@ body {
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scrollbar-color: var(--grad-center) var(--bg-mid);
scrollbar-width: thin;
}
#root {
min-height: 100vh;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-mid);
}
::-webkit-scrollbar-thumb {
background: var(--grad-center);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--highlight);
}

View File

@@ -0,0 +1,3 @@
export { TIERS, TIER_MIN, TIER_MAX, findTier, progressPercent } from './model/tiers'
export type { Tier } from './model/tiers'
export { CommissionTable } from './ui/CommissionTable'

View File

@@ -6,8 +6,8 @@ export interface Tier {
export const TIERS: readonly Tier[] = [
{ min: 5_000, max: 30_000, pct: 8 },
{ min: 30_000, max: 100_000, pct: 6 },
{ min: 100_000, max: 600_000, pct: 4 },
{ min: 30_001, max: 100_000, pct: 6 },
{ min: 100_001, max: 600_000, pct: 4 },
] as const
export const TIER_MIN = TIERS[0].min

View File

@@ -1,5 +1,5 @@
import { TIERS } from '@widgets/currency-converter'
import styles from './CommissionPanel.module.css'
import { TIERS } from '../model/tiers'
import styles from './CommissionTable.module.css'
const ru = (n: number) => n.toLocaleString('ru-RU')
@@ -10,7 +10,7 @@ interface Props {
effectiveRate: number
}
export function CommissionPanel({ amount, progress, commission, effectiveRate }: Props) {
export function CommissionTable({ amount, progress, commission, effectiveRate }: Props) {
return (
<div>
<div className={styles.title}>КОМИССИЯ СЕРВИСА</div>

View File

@@ -0,0 +1,179 @@
import type {
AdminLoginRequest,
AdminLoginResponse,
AdminMeResponse,
CreateOrganizationRequest,
DocumentResponse,
Organization,
OrganizationListResponse,
PurchaseRequestListResponse,
UpdateOrganizationRequest,
WalletResponse,
} from '../model/types'
const ADMIN_API_URL = 'https://app.admin.elcsa.ru'
// In-memory admin access token — deliberately separate from the user `tokenStore`
// so the two independent auth systems never collide. No CSRF on the admin API.
let adminToken: string | null = null
export const adminTokenStore = {
get: () => adminToken,
set: (token: string) => { adminToken = token },
clear: () => { adminToken = null },
}
async function doAdminRequest<T>(
path: string,
options: RequestInit,
allowRetry: boolean,
): Promise<T> {
const bearer = adminTokenStore.get()
// For multipart uploads we must NOT set Content-Type — the browser adds the
// boundary itself. Detect FormData bodies and skip the JSON header.
const isFormData = options.body instanceof FormData
const res = await fetch(`${ADMIN_API_URL}${path}`, {
...options,
credentials: 'include',
headers: {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
...options.headers,
},
})
if (res.status === 401 && allowRetry) {
try {
await refreshAdminToken()
return doAdminRequest<T>(path, options, false)
} catch {
adminTokenStore.clear()
throw new Error('Unauthorized')
}
}
const data = await res.json().catch(() => null)
if (!res.ok) throw data
return data as T
}
// Refresh by analogy with the main auth service: HttpOnly refresh cookie -> fresh access token.
// Exact path is isolated here — adjust if the backend differs (e.g. /v1/jwt/refresh).
export async function refreshAdminToken(): Promise<string> {
const res = await fetch(`${ADMIN_API_URL}/v1/auth/refresh`, {
method: 'POST',
credentials: 'include',
})
if (!res.ok) throw new Error('Unauthorized')
const data = await res.json()
if (data.access_token) adminTokenStore.set(data.access_token)
return (data.access_token ?? true) as string
}
export async function adminLogin(payload: AdminLoginRequest): Promise<AdminLoginResponse> {
const data = await doAdminRequest<AdminLoginResponse>(
'/v1/auth/login',
{ method: 'POST', body: JSON.stringify(payload) },
false,
)
if (data.access_token) adminTokenStore.set(data.access_token)
return data
}
export function getAdminMe(): Promise<AdminMeResponse> {
return doAdminRequest<AdminMeResponse>('/v1/auth/me', {}, true)
}
export async function adminLogout(): Promise<void> {
try {
await doAdminRequest<unknown>('/v1/auth/logout', { method: 'POST' }, false)
} finally {
adminTokenStore.clear()
}
}
export function getOrganizations(limit = 50, offset = 0): Promise<OrganizationListResponse> {
return doAdminRequest<OrganizationListResponse>(
`/v1/organizations?limit=${limit}&offset=${offset}`,
{},
true,
)
}
export function createOrganization(payload: CreateOrganizationRequest): Promise<Organization> {
return doAdminRequest<Organization>(
'/v1/organizations',
{ method: 'POST', body: JSON.stringify(payload) },
true,
)
}
export function getOrganization(id: string): Promise<Organization> {
return doAdminRequest<Organization>(`/v1/organizations/${id}`, {}, true)
}
export function createOrganizationWallets(id: string): Promise<WalletResponse[]> {
return doAdminRequest<WalletResponse[]>(
`/v1/organizations/${id}/wallets/create`,
{ method: 'POST' },
true,
)
}
export function getDocuments(orgId: string): Promise<DocumentResponse[]> {
return doAdminRequest<DocumentResponse[]>(
`/v1/organizations/${orgId}/documents`,
{},
true,
)
}
export function uploadDocument(
orgId: string,
documentType: string,
file: File,
): Promise<DocumentResponse> {
const body = new FormData()
body.append('document_type', documentType)
body.append('file', file)
return doAdminRequest<DocumentResponse>(
`/v1/organizations/${orgId}/documents`,
{ method: 'POST', body },
true,
)
}
export async function getPurchaseRequests(params: {
organizationId?: string
status?: string
limit?: number
offset?: number
}): Promise<PurchaseRequestListResponse> {
const query = new URLSearchParams()
if (params.organizationId) query.set('organization_id', params.organizationId)
if (params.status) query.set('status', params.status)
query.set('limit', String(params.limit ?? 50))
query.set('offset', String(params.offset ?? 0))
const data = await doAdminRequest<PurchaseRequestListResponse>(
`/v1/purchase-requests?${query.toString()}`,
{},
true,
)
// TEMP: inspect real backend shape — especially which `status` values appear.
console.log('[purchase-requests] list response:', data)
return data
}
export function updateOrganization(
id: string,
payload: UpdateOrganizationRequest,
): Promise<Organization> {
return doAdminRequest<Organization>(
`/v1/organizations/${id}`,
{ method: 'PATCH', body: JSON.stringify(payload) },
true,
)
}

View File

@@ -0,0 +1,16 @@
import { useQuery } from '@tanstack/react-query'
import { refreshAdminToken } from '../api/adminApi'
export const ADMIN_AUTH_QUERY_KEY = ['admin-auth']
export function useAdminAuth(): { isAuthenticated: boolean; isLoading: boolean } {
const { data, isLoading, isError } = useQuery({
queryKey: ADMIN_AUTH_QUERY_KEY,
queryFn: refreshAdminToken,
retry: false,
staleTime: Infinity,
gcTime: Infinity,
refetchOnWindowFocus: false,
})
return { isAuthenticated: !!data && !isError, isLoading }
}

View File

@@ -0,0 +1,15 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { adminLogin } from '../api/adminApi'
import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth'
export function useAdminLogin() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: adminLogin,
onSuccess: (data) => {
// The token is already stored by adminLogin; write it straight into the
// gate's query cache so we flip to "authenticated" without re-hitting /refresh.
queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, data.access_token)
},
})
}

View File

@@ -0,0 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { adminLogout } from '../api/adminApi'
import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth'
export function useAdminLogout() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: adminLogout,
onSuccess: () => {
// Flip the gate back to "not authenticated" without triggering a /refresh refetch.
queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, null)
},
})
}

View File

@@ -0,0 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createOrganization } from '../api/adminApi'
import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations'
export function useCreateOrganization() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createOrganization,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY })
},
})
}

View File

@@ -0,0 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createOrganizationWallets } from '../api/adminApi'
import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations'
import { ORGANIZATION_QUERY_KEY } from './useOrganization'
export function useCreateOrganizationWallets() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (organizationId: string) => createOrganizationWallets(organizationId),
onSuccess: (_wallets, organizationId) => {
// `has_wallets` flips to true server-side — refresh list and detail view.
queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY })
queryClient.invalidateQueries({ queryKey: ORGANIZATION_QUERY_KEY(organizationId) })
},
})
}

View File

@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query'
import { getDocuments } from '../api/adminApi'
export const DOCUMENTS_QUERY_KEY = (orgId: string) => ['admin-documents', orgId]
export function useDocuments(orgId: string | undefined) {
return useQuery({
queryKey: DOCUMENTS_QUERY_KEY(orgId ?? ''),
queryFn: () => getDocuments(orgId as string),
enabled: !!orgId,
})
}

View File

@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query'
import { getOrganization } from '../api/adminApi'
export const ORGANIZATION_QUERY_KEY = (id: string) => ['admin-organization', id]
export function useOrganization(id: string | undefined) {
return useQuery({
queryKey: ORGANIZATION_QUERY_KEY(id ?? ''),
queryFn: () => getOrganization(id as string),
enabled: !!id,
})
}

View File

@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { getOrganizations } from '../api/adminApi'
export const ORGANIZATIONS_QUERY_KEY = ['admin-organizations']
export function useOrganizations() {
return useQuery({
queryKey: ORGANIZATIONS_QUERY_KEY,
queryFn: () => getOrganizations(),
})
}

View File

@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { getPurchaseRequests } from '../api/adminApi'
export const PURCHASE_REQUESTS_QUERY_KEY = (orgId: string) => [
'admin-purchase-requests',
orgId,
]
export function usePurchaseRequests(orgId: string | undefined) {
return useQuery({
queryKey: PURCHASE_REQUESTS_QUERY_KEY(orgId ?? ''),
queryFn: () => getPurchaseRequests({ organizationId: orgId }),
enabled: !!orgId,
})
}

View File

@@ -0,0 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updateOrganization } from '../api/adminApi'
import type { UpdateOrganizationRequest } from '../model/types'
import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations'
import { ORGANIZATION_QUERY_KEY } from './useOrganization'
export function useUpdateOrganization(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload: UpdateOrganizationRequest) => updateOrganization(id, payload),
onSuccess: (data) => {
queryClient.setQueryData(ORGANIZATION_QUERY_KEY(id), data)
queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY })
},
})
}

View File

@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { uploadDocument } from '../api/adminApi'
import { DOCUMENTS_QUERY_KEY } from './useDocuments'
interface UploadArgs {
documentType: string
file: File
}
export function useUploadDocument(orgId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ documentType, file }: UploadArgs) =>
uploadDocument(orgId, documentType, file),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: DOCUMENTS_QUERY_KEY(orgId) })
},
})
}

View File

@@ -0,0 +1,40 @@
export {
adminLogin,
adminLogout,
getAdminMe,
getOrganizations,
getOrganization,
createOrganization,
createOrganizationWallets,
updateOrganization,
getDocuments,
uploadDocument,
getPurchaseRequests,
refreshAdminToken,
adminTokenStore,
} from './api/adminApi'
export type {
AdminLoginRequest,
AdminLoginResponse,
AdminMeResponse,
Organization,
OrganizationListResponse,
CreateOrganizationRequest,
UpdateOrganizationRequest,
WalletResponse,
DocumentResponse,
PurchaseRequestResponse,
PurchaseRequestListResponse,
BankDetails,
} from './model/types'
export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth'
export { useAdminLogin } from './hooks/useAdminLogin'
export { useAdminLogout } from './hooks/useAdminLogout'
export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations'
export { useOrganization, ORGANIZATION_QUERY_KEY } from './hooks/useOrganization'
export { useCreateOrganization } from './hooks/useCreateOrganization'
export { useCreateOrganizationWallets } from './hooks/useCreateOrganizationWallets'
export { useUpdateOrganization } from './hooks/useUpdateOrganization'
export { useDocuments, DOCUMENTS_QUERY_KEY } from './hooks/useDocuments'
export { useUploadDocument } from './hooks/useUploadDocument'
export { usePurchaseRequests, PURCHASE_REQUESTS_QUERY_KEY } from './hooks/usePurchaseRequests'

View File

@@ -0,0 +1,125 @@
export interface AdminLoginRequest {
login: string
password: string
}
export interface AdminLoginResponse {
access_token: string
token_type: string
id: string
login: string
first_name: string | null
last_name: string | null
role: string
}
export interface AdminMeResponse {
id: string
login: string
first_name: string | null
last_name: string | null
role: string
}
export type BankDetails = Record<string, unknown>
export interface Organization {
id: string
user_id: string
name: string
short_name: string | null
inn: string
ogrn: string | null
kpp: string | null
legal_address: string | null
actual_address: string | null
bank_details: BankDetails | null
contact_person: string | null
contact_phone: string | null
status: string
kyc_verified: boolean
kyc_verified_at: string | null
has_wallets: boolean
created_by: string | null
created_at: string | null
updated_at: string | null
}
export interface OrganizationListResponse {
items: Organization[]
total: number
}
export interface WalletResponse {
id: string
chain: string
address: string
derivation_path: string
created_at: string | null
}
export interface DocumentResponse {
id: string
organization_id: string
document_type: string
file_name: string
content_type: string
file_size_bytes: number
uploaded_by: string | null
created_at: string | null
download_url: string | null
}
// Monetary fields are strings to preserve decimal precision — do not coerce to number.
export interface PurchaseRequestResponse {
id: string
organization_id: string
status: string
usdt_amount: string
rub_amount: string | null
exchange_rate: string | null
service_fee_percent: string | null
comment: string | null
admin_comment: string | null
target_wallet_chain: string | null
target_wallet_address: string | null
tx_hash: string | null
assigned_to: string | null
created_at: string | null
updated_at: string | null
completed_at: string | null
}
export interface PurchaseRequestListResponse {
items: PurchaseRequestResponse[]
total: number
}
export interface UpdateOrganizationRequest {
name?: string | null
short_name?: string | null
ogrn?: string | null
kpp?: string | null
legal_address?: string | null
actual_address?: string | null
bank_details?: BankDetails | null
contact_person?: string | null
contact_phone?: string | null
status?: string | null
}
export interface CreateOrganizationRequest {
email: string
password: string
name: string
inn: string
short_name?: string | null
ogrn?: string | null
kpp?: string | null
legal_address?: string | null
actual_address?: string | null
bank_details?: BankDetails | null
contact_person?: string | null
contact_phone?: string | null
status?: string
}

View File

@@ -1,37 +1,159 @@
import { getCsrfToken } from '@shared/api/csrf'
import { tokenStore } from '@shared/api/tokenStore'
const USERS_API_URL = 'https://app.users.elcsa.ru'
export type AccountType = 'individual' | 'legal_entity'
// Nested organization payload — present only on legal_entity accounts.
export interface LegalEntityInfo {
id: string
name: string
inn: string
status: string
short_name: string | null
ogrn: string | null
kpp: string | null
legal_address: string | null
actual_address: string | null
bank_details: Record<string, unknown> | null
contact_person: string | null
contact_phone: string | null
kyc_verified: boolean
kyc_verified_at: string | null
}
export interface MeResponse {
id: string
email: string
first_name: string
middle_name: string
last_name: string
birth_date: string
crypto_wallet: string | null
phone: string
// Person fields are null on legal_entity accounts.
first_name: string | null
middle_name: string | null
last_name: string | null
birth_date: string | null
encrypted_mnemonic: string | null
phone: string | null
passport_data: string | null
inn: string | null
erc20: string | null
avatar_link: string | null
kyc_verified: boolean
is_deleted: boolean
created_at: string
updated_at: string
kyc_verified_at: string | null
webp_size_bytes?: number
// "individual" -> физлицо, "legal_entity" -> аккаунт юр.лица.
account_type: AccountType
// Populated only for legal_entity accounts.
legal_entity?: LegalEntityInfo | null
}
export interface UploadAvatarPayload {
photo_base64: string
decoded_bytes: string
}
async function authedHeaders(): Promise<HeadersInit> {
const csrf = await getCsrfToken()
const bearer = tokenStore.get()
return {
'X-CSRF-Token': csrf,
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
}
}
export async function getMe(): Promise<MeResponse> {
const csrf = await getCsrfToken()
const headers = await authedHeaders()
const res = await fetch(`${USERS_API_URL}/me/`, {
credentials: 'include',
headers: {
'X-CSRF-Token': csrf,
},
headers,
})
const data = await res.json()
if (!res.ok) throw data
return data
}
export async function uploadAvatar(payload: UploadAvatarPayload): Promise<MeResponse> {
const headers = await authedHeaders()
const res = await fetch(`${USERS_API_URL}/me/settings/avatar`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(payload),
})
const data = await res.json()
if (!res.ok) throw data
return data
}
export interface PasswordResetStartPayload {
email: string
}
export async function passwordResetStart(payload: PasswordResetStartPayload): Promise<void> {
const csrf = await getCsrfToken()
const res = await fetch(`${USERS_API_URL}/me/settings/password/forgot/start`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
},
body: JSON.stringify(payload),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw data
}
}
export async function updatePhone(phone: string): Promise<void> {
const headers = await authedHeaders()
const res = await fetch(`${USERS_API_URL}/me/settings/phone`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({ phone }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw data
}
}
export interface PasswordResetCompletePayload {
email: string
code: string
new_password: string
confirm_password: string
}
export async function passwordResetComplete(payload: PasswordResetCompletePayload): Promise<void> {
const csrf = await getCsrfToken()
const res = await fetch(`${USERS_API_URL}/me/settings/password/forgot/complete`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
},
body: JSON.stringify(payload),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw data
}
}

View File

@@ -1,13 +1,17 @@
import { useQuery } from '@tanstack/react-query'
import type { UseQueryOptions } from '@tanstack/react-query'
import { getMe } from '../api/profileApi'
import type { MeResponse } from '../api/profileApi'
export function useMe() {
type MeOptions = Pick<UseQueryOptions<MeResponse>, 'refetchInterval' | 'enabled'>
export function useMe(options?: MeOptions) {
return useQuery<MeResponse>({
queryKey: ['me'],
queryFn: getMe,
staleTime: Infinity,
gcTime: Infinity,
retry: false,
...options,
})
}

View File

@@ -0,0 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updatePhone } from '../api/profileApi'
export function useUpdatePhone() {
const queryClient = useQueryClient()
return useMutation<void, unknown, string>({
mutationFn: updatePhone,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['me'] })
},
})
}

View File

@@ -0,0 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { uploadAvatar } from '../api/profileApi'
import type { MeResponse, UploadAvatarPayload } from '../api/profileApi'
export function useUploadAvatar() {
const queryClient = useQueryClient()
return useMutation<MeResponse, unknown, UploadAvatarPayload>({
mutationFn: uploadAvatar,
onSuccess: (data) => {
queryClient.setQueryData(['me'], data)
},
})
}

View File

@@ -1,7 +1,9 @@
export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi'
export { getMe } from './api/profileApi'
export type { MeResponse } from './api/profileApi'
export { getMe, uploadAvatar, updatePhone } from './api/profileApi'
export type { MeResponse, UploadAvatarPayload } from './api/profileApi'
export { useMe } from './hooks/useMe'
export { useUploadAvatar } from './hooks/useUploadAvatar'
export { useUpdatePhone } from './hooks/useUpdatePhone'
export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi'
export { useIsAuthenticated } from './hooks/useIsAuthenticated'
export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth'

View File

@@ -62,6 +62,10 @@ export function getPaymentQuote(usdtAmount: number): Promise<PaymentQuote> {
return doPaymentRequest(`/payment/quote?usdt_amount=${usdtAmount}`, {}, true)
}
export function getPaymentQuoteByRub(rubAmount: number): Promise<PaymentQuote> {
return doPaymentRequest(`/payment/quote/rub?total_rub=${rubAmount}`, {}, true)
}
export interface CreateOrderPayload {
usdt_amount: number
usdt_exchange_rate: number
@@ -103,3 +107,71 @@ export function createOrder(payload: CreateOrderPayload): Promise<OrderResult> {
body: JSON.stringify(payload),
}, true)
}
export type OrderStatus = 'pending' | 'rejected' | 'completed' | 'cancelled' | 'error'
export type PaymentStatus =
| 'pending'
| 'money_accepted'
| 'web3_processing'
| 'web3_hash_error'
| 'web3_balance_problem'
| 'receipt_error'
| 'completed'
| 'usdt_delivered'
export interface Order {
id: string
created_at: string
updated_at: string
user_id: string
usdt_amount: string
usdt_exchange_rate: string
gas_fee: string
total_price: string
service_fee: string
status: OrderStatus
client_payment_id: string
itpay_payment_qr_url_desktop: string
itpay_payment_qr_url_android: string
itpay_payment_qr_url_ios: string
itpay_payment_qr_image_desktop: string
itpay_payment_qr_image_android: string
itpay_payment_qr_image_ios: string
itpay_id: string
itpay_qr_id: string
itpay_amount: string
itpay_created_at: string
}
export interface Payment {
id: string
created_at: string
updated_at: string
user_id: string
order_id: string
status: PaymentStatus
receipt_cloudekassir_id: string
receipt_cloudekassir_link: string
itpay_payment_id: string
itpay_paid_amount: string
transaction_id: string
web3_transaction_hash: string
paid_at: string
expired_date: string
}
export interface OrderWithPayment {
order: Order
payment: Payment | null
}
export interface OrdersResponse {
orders: OrderWithPayment[]
}
export const ORDERS_LIMIT = 20
export function getOrders(offset: number, limit: number = ORDERS_LIMIT): Promise<OrdersResponse> {
return doPaymentRequest(`/payment/orders?offset=${offset}&limit=${limit}`, {}, true)
}

View File

@@ -0,0 +1,15 @@
import { useInfiniteQuery } from '@tanstack/react-query'
import { getOrders, ORDERS_LIMIT } from '../api/paymentApi'
export function useOrders() {
return useInfiniteQuery({
queryKey: ['payment', 'orders'],
queryFn: ({ pageParam }) => getOrders(pageParam as number),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.orders.length < ORDERS_LIMIT) return undefined
return allPages.length * ORDERS_LIMIT
},
staleTime: 30_000,
})
}

View File

@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query'
import { getPaymentQuoteByRub } from '../api/paymentApi'
import type { PaymentQuote } from '../api/paymentApi'
export function usePaymentQuoteByRub(rubAmount: number) {
return useQuery<PaymentQuote>({
queryKey: ['payment', 'quote', 'rub', rubAmount],
queryFn: () => getPaymentQuoteByRub(rubAmount),
enabled: rubAmount > 0,
staleTime: 30_000,
retry: false,
})
}

View File

@@ -1,4 +1,7 @@
export { usePaymentConfig } from './hooks/usePaymentConfig'
export { usePaymentQuote } from './hooks/usePaymentQuote'
export { usePaymentQuoteByRub } from './hooks/usePaymentQuoteByRub'
export { useCreateOrder } from './hooks/useCreateOrder'
export type { PaymentConfig, PaymentQuote, CreateOrderPayload, OrderResult } from './api/paymentApi'
export { useOrders } from './hooks/useOrders'
export { useCurrencyConversion } from './model/useCurrencyConversion'
export type { PaymentConfig, PaymentQuote, CreateOrderPayload, OrderResult, Order, Payment, OrderWithPayment, OrderStatus, PaymentStatus } from './api/paymentApi'

View File

@@ -0,0 +1,102 @@
import { useState } from 'react'
import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { progressPercent } from '@entities/commission'
import { GAS_PRICE, MIN_RUB_AMOUNT } from '@shared/config/constants'
import type { ConvertFieldData } from '@shared/ui'
import { usePaymentConfig } from '../hooks/usePaymentConfig'
import { usePaymentQuote } from '../hooks/usePaymentQuote'
import { usePaymentQuoteByRub } from '../hooks/usePaymentQuoteByRub'
const TOO_LARGE_ERROR = 'Сумма слишком большая и превышает 600 000 ₽'
const sanitize = (raw: string) => raw.replace(/[^0-9.]/g, '')
interface Options {
/** Значение курса USDT/RUB до загрузки конфига (пилюля). */
rateFallback?: number
}
export function useCurrencyConversion({ rateFallback = 0 }: Options = {}) {
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
const [usdtInput, setUsdtInput] = useState('1000')
const [rubInput, setRubInput] = useState(String(MIN_RUB_AMOUNT))
const { data: config } = usePaymentConfig()
const configUsdtRate = Number(config?.usdt_exchange_rate) || rateFallback
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
const isUsdtToRub = direction === 'usdt_to_rub'
const numUsdt = Number.parseFloat(usdtInput) || 0
const debouncedUsdt = useDebounce(numUsdt, 400)
const { data: quoteUsdtToRub, isError: quoteError } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
const numRubInput = Number.parseFloat(rubInput) || 0
const debouncedRub = useDebounce(numRubInput, 400)
const { data: quoteRubToUsdt, isError: quoteRubError } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
const rubBelowMin = !isUsdtToRub && numRubInput > 0 && numRubInput < MIN_RUB_AMOUNT
const rubTotal = quoteUsdtToRub?.total_price ?? ''
const rubTotalNum = Number(rubTotal) || 0
const usdtFromRub = quoteRubToUsdt?.usdt_amount ?? ''
const usdtFromRubNum = Number(usdtFromRub) || 0
const commission = isUsdtToRub
? Number(quoteUsdtToRub?.service_fee) || 0
: Number(quoteRubToUsdt?.service_fee) || 0
const displayRubAmount = isUsdtToRub ? rubTotalNum : numRubInput
const effectiveRate = isUsdtToRub
? (numUsdt > 0 ? rubTotalNum / numUsdt : 0)
: (usdtFromRubNum > 0 ? numRubInput / usdtFromRubNum : 0)
function onSwap() {
setDirection(d => (d === 'usdt_to_rub' ? 'rub_to_usdt' : 'usdt_to_rub'))
}
const convert: ConvertFieldData = isUsdtToRub
? {
value: usdtInput,
currency: 'USDT',
onChange: (raw) => setUsdtInput(sanitize(raw)),
error: quoteError ? TOO_LARGE_ERROR : undefined,
}
: {
value: rubInput,
currency: 'RUB',
onChange: (raw) => setRubInput(sanitize(raw)),
error: rubBelowMin
? `Минимальная сумма: ${MIN_RUB_AMOUNT.toLocaleString('ru-RU')}`
: quoteRubError
? TOO_LARGE_ERROR
: undefined,
}
const pay: ConvertFieldData = isUsdtToRub
? { value: rubTotal, currency: 'RUB' }
: { value: usdtFromRub, currency: 'USDT' }
return {
isUsdtToRub,
gasPriceRub,
configUsdtRate,
convert,
pay,
onSwap,
commission: {
amount: displayRubAmount,
progress: progressPercent(displayRubAmount),
commission,
effectiveRate,
},
// сырые значения для создания ордера и валидации в обёртках
numUsdt,
usdtFromRubNum,
rubTotal,
rubTotalNum,
numRubInput,
usdtFromRub,
rubBelowMin,
}
}

View File

@@ -1,4 +1,7 @@
import { api } from '@shared/api/base'
import { getCsrfToken } from '@shared/api/csrf'
import { tokenStore, refreshAccessToken } from '@shared/api/tokenStore'
const WALLET_API_URL = 'https://app.cryptowallet.elcsa.ru'
export type Chain = 'ETH' | 'BSC' | 'BTC' | 'TRX' | 'SOL'
@@ -21,16 +24,395 @@ export interface PriceEntry {
usd: number
}
export interface SendWalletPayload {
to: string
amount: string
token?: string
feeTier?: 'slow' | 'normal' | 'fast'
}
export interface SendWalletResponse {
data: { txid: string; chain: Chain }
}
export interface WalletAddress {
chain: Chain
address: string
derivationPath: string
}
export interface PortfolioChain {
chain: Chain
address: string
native: FormattedAmount
tokens: Record<string, FormattedAmount>
totalUsd: number
stale: boolean
lastUpdated: number
}
export interface PortfolioData {
totalUsd: number
hasErrors: boolean
perChain: Record<Chain, PortfolioChain>
}
export const CHAINS: Chain[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL']
async function walletGet<T>(path: string, allowRetry: boolean = true): Promise<T> {
const csrf = await getCsrfToken()
const bearer = tokenStore.get()
const res = await fetch(`${WALLET_API_URL}${path}`, {
credentials: 'include',
headers: {
'X-CSRF-Token': csrf,
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
},
})
if (res.status === 401 && allowRetry) {
try {
await refreshAccessToken()
return walletGet<T>(path, false)
} catch {
tokenStore.clear()
throw new Error('Unauthorized')
}
}
const data = await res.json()
if (!res.ok) throw data
return data as T
}
async function walletPost<T>(
path: string,
body: unknown,
allowRetry: boolean = true,
extraHeaders: Record<string, string> = {}
): Promise<T> {
const csrf = await getCsrfToken()
const bearer = tokenStore.get()
const res = await fetch(`${WALLET_API_URL}${path}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
...extraHeaders,
},
body: JSON.stringify(body),
})
if (res.status === 401 && allowRetry) {
try {
await refreshAccessToken()
return walletPost<T>(path, body, false, extraHeaders)
} catch {
tokenStore.clear()
throw new Error('Unauthorized')
}
}
const data = await res.json()
if (!res.ok) throw data
return data as T
}
export async function getWalletAddresses(): Promise<WalletAddress[]> {
const res = await walletGet<{ success: boolean; data: WalletAddress[] }>('/api/wallets')
return res.data
}
export async function getWalletBalance(chain: Chain): Promise<WalletBalanceData> {
const res = await api.get<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`)
const res = await walletGet<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`)
return res.data
}
export async function getPrices(symbols: string[]): Promise<Record<string, PriceEntry>> {
const res = await api.get<{ success: boolean; data: Record<string, PriceEntry> }>(
const res = await walletGet<{ success: boolean; data: Record<string, PriceEntry> }>(
`/api/prices?symbols=${symbols.join(',')}`
)
return res.data
}
export async function sendWallet(chain: Chain, payload: SendWalletPayload): Promise<SendWalletResponse> {
return walletPost<SendWalletResponse>(`/api/wallets/${chain}/send`, payload)
}
export async function getPortfolio(): Promise<PortfolioData> {
const res = await walletGet<{ success: boolean; data: PortfolioData }>('/api/wallets/portfolio')
return res.data
}
export interface TokenInfo {
chain: string
symbol: string
name: string
contract: string | null
}
export interface RelayQuotePayload {
user: string
recipient: string
originChainId: number
destinationChainId: number
originCurrency: string
destinationCurrency: string
amount: string
tradeType: 'EXACT_INPUT'
}
export interface RelayQuoteResponse {
details: {
currencyOut: {
amountFormatted: string
amountUsd: string
}
}
fees: {
gas: {
amountUsd: string
}
}
}
export async function getTokensList(): Promise<TokenInfo[]> {
const res = await walletGet<{ success: boolean; data: TokenInfo[] }>('/api/tokens')
return res.data
}
export interface JumperToken {
address: string
chainId: number
symbol: string
decimals: number
name: string
coinKey?: string
logoURI?: string
priceUSD: string
}
export type JumperTokensMap = Record<string, JumperToken[]>
export async function getJumperTokens(): Promise<JumperTokensMap> {
const res = await walletGet<{ tokens?: JumperTokensMap; data?: { tokens: JumperTokensMap } }>(
'/api/jumper/tokens?chains=1,56,1151111081099710,728126428,20000000000001'
)
return res.data?.tokens ?? res.tokens ?? {}
}
export interface JumperQuotePayload {
fromChain: string
toChain: string
fromToken: string
toToken: string
fromAmount: string
fromAddress: string
toAddress: string
slippage: number
}
export interface JumperQuoteToken {
address: string
chainId: number
symbol: string
decimals: number
name: string
logoURI?: string
priceUSD: string
}
export interface JumperFeeCost {
name: string
description?: string
token: JumperQuoteToken
amount: string
amountUSD: string
percentage?: string
included?: boolean
}
export interface JumperQuote {
type: string
id: string
tool: string
toolDetails: { key: string; name: string; logoURI?: string }
action: {
fromToken: JumperQuoteToken
fromAmount: string
toToken: JumperQuoteToken
fromChainId: number
toChainId: number
slippage: number
fromAddress: string
toAddress: string
}
estimate: {
tool: string
approvalAddress?: string
toAmountMin: string
toAmount: string
fromAmount: string
feeCosts?: JumperFeeCost[]
}
}
export async function getJumperQuote(payload: JumperQuotePayload): Promise<JumperQuote> {
const qs = new URLSearchParams({
fromChain: payload.fromChain,
toChain: payload.toChain,
fromToken: payload.fromToken,
toToken: payload.toToken,
fromAmount: payload.fromAmount,
fromAddress: payload.fromAddress,
toAddress: payload.toAddress,
slippage: String(payload.slippage),
}).toString()
const res = await walletGet<JumperQuote & { body?: JumperQuote; data?: { body?: JumperQuote } }>(
`/api/jumper/quote-best?${qs}`
)
return (res.data?.body ?? res.body ?? res) as JumperQuote
}
export interface BridgeExecutePayload {
provider: string
fromChain: number
toChain: number
fromToken: string
toToken: string
fromAmount: string
fromAddress: string
toAddress: string
acceptedMinOut?: string
}
export interface BridgeExecuteResult {
provider: string
fromChain: number
toChain: number
toolName: string
feeTxid?: string
feeAmount?: string
bridgeTxid: string
fromAmount: string
toAmountMin: string
fromAmountUSD?: string
toAmountUSD?: string
trackerUrl?: string
}
export async function executeBridge(payload: BridgeExecutePayload): Promise<BridgeExecuteResult> {
const res = await walletPost<{ data?: { success: boolean; data: BridgeExecuteResult } }>(
'/api/bridge/execute',
payload,
true,
{ 'Idempotency-Key': crypto.randomUUID() }
)
return (res.data?.data ?? res) as BridgeExecuteResult
}
export async function getRelayQuote(payload: RelayQuotePayload): Promise<RelayQuoteResponse> {
return walletPost<RelayQuoteResponse>('/api/relay/quote', payload)
}
export interface RelaySwapStep {
id: string
action: string
description: string
kind: string
items: Array<{
status: string
data: {
from: string
to: string
data: string
value: string
chainId: number
gas: string
maxFeePerGas: string
maxPriorityFeePerGas: string
}
check: {
endpoint: string
method: string
}
}>
requestId: string
}
export interface RelaySwapResponse {
steps: RelaySwapStep[]
fees: RelayQuoteResponse['fees']
details: {
operation: string
sender: string
recipient: string
currencyIn: { amount: string; amountFormatted: string; amountUsd: string; currency: { symbol: string } }
currencyOut: { amount: string; amountFormatted: string; amountUsd: string; currency: { symbol: string } }
totalImpact: { usd: string; percent: string }
rate: string
timeEstimate: number
}
}
export async function executeRelaySwap(payload: RelayQuotePayload): Promise<RelaySwapResponse> {
return walletPost<RelaySwapResponse>('/api/relay/execute/swap', payload)
}
export async function signRawEvmTx(
chain: 'ETH' | 'BSC',
txData: RelaySwapStep['items'][0]['data']
): Promise<unknown> {
const key = `relay-${chain.toLowerCase()}-${Date.now()}`
return walletPost(`/api/wallets/${chain}/sign-raw-evm-tx`, txData, true, { 'Idempotency-Key': key })
}
export async function signSolTx(txData: unknown): Promise<unknown> {
return walletPost('/api/wallets/SOL/sign-and-broadcast-tx', txData)
}
export interface TrxSwapQuotePayload {
from: string
to: string
amountHuman: string
}
export interface TrxSwapQuoteData {
quoteId: string
expiresIn: number
expectedOutFormatted: string
minOutFormatted: string
fees: {
network: { amountFormatted: string; asset: string; amountUsd: number }
}
}
export async function getTrxSwapQuote(payload: TrxSwapQuotePayload): Promise<TrxSwapQuoteData> {
const res = await walletPost<{ success: boolean; data: TrxSwapQuoteData }>(
'/api/wallets/TRX/swap/quote',
payload
)
return res.data
}
export async function executeTrxSwap(quoteId: string): Promise<unknown> {
return walletPost(
'/api/wallets/TRX/swap',
{ quoteId },
true,
{ 'Idempotency-Key': `trx-${Date.now()}` }
)
}
export async function createWallet(): Promise<void> {
await walletPost<unknown>('/api/wallets/create', {})
}
export async function revealMnemonic(): Promise<string> {
const res = await walletPost<{ success: boolean; data: { mnemonic: string } }>('/api/wallets/mnemonic/reveal', { confirm: 'I_UNDERSTAND_SEED_IS_SECRET' })
return res.data.mnemonic
}

View File

@@ -1,3 +1,3 @@
export { useAllWalletBalances, usePrices } from './model/useWalletData'
export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry } from './api/walletApi'
export { useAllWalletBalances, usePrices, useSendWallet, useWalletAddresses, useWalletBalance, usePortfolio, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, useJumperTokens, useJumperQuote, useFetchJumperQuote, useExecuteBridge, useCreateWallet, useRevealMnemonic } from './model/useWalletData'
export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry, SendWalletPayload, SendWalletResponse, WalletAddress, PortfolioData, PortfolioChain, TokenInfo, RelayQuotePayload, RelayQuoteResponse, RelaySwapResponse, RelaySwapStep, TrxSwapQuotePayload, TrxSwapQuoteData, JumperToken, JumperTokensMap, JumperQuote, JumperQuotePayload, JumperQuoteToken, JumperFeeCost, BridgeExecutePayload, BridgeExecuteResult } from './api/walletApi'
export { CHAINS } from './api/walletApi'

View File

@@ -1,5 +1,13 @@
import { useQuery, useQueries } from '@tanstack/react-query'
import { getWalletBalance, getPrices, CHAINS } from '../api/walletApi'
import { useQuery, useQueries, useMutation } from '@tanstack/react-query'
import { getWalletBalance, getPrices, sendWallet, getWalletAddresses, getPortfolio, getTokensList, getRelayQuote, executeRelaySwap, signRawEvmTx, signSolTx, getTrxSwapQuote, executeTrxSwap, getJumperTokens, getJumperQuote, executeBridge, createWallet, revealMnemonic, CHAINS, type Chain, type SendWalletPayload, type RelayQuotePayload, type RelaySwapStep, type TrxSwapQuotePayload, type JumperQuotePayload, type BridgeExecutePayload } from '../api/walletApi'
export function useWalletBalance(chain: Chain) {
return useQuery({
queryKey: ['wallet', 'balance', chain],
queryFn: () => getWalletBalance(chain),
staleTime: 30_000,
})
}
export function useAllWalletBalances() {
return useQueries({
@@ -18,3 +26,120 @@ export function usePrices(symbols: string[]) {
staleTime: 5 * 60 * 1000,
})
}
export function useSendWallet() {
return useMutation({
mutationFn: ({ chain, ...payload }: { chain: Chain } & SendWalletPayload) =>
sendWallet(chain, payload),
})
}
export function useWalletAddresses() {
return useQuery({
queryKey: ['wallet', 'addresses'],
queryFn: getWalletAddresses,
staleTime: 10 * 60 * 1000,
})
}
export function usePortfolio() {
return useQuery({
queryKey: ['wallet', 'portfolio'],
queryFn: getPortfolio,
staleTime: 30_000,
})
}
export function useTokensList() {
return useQuery({
queryKey: ['wallet', 'tokens'],
queryFn: getTokensList,
staleTime: 10 * 60 * 1000,
})
}
export function useJumperTokens() {
return useQuery({
queryKey: ['wallet', 'jumper', 'tokens'],
queryFn: getJumperTokens,
staleTime: 10 * 60 * 1000,
})
}
export function useJumperQuote(payload: JumperQuotePayload | null) {
return useQuery({
queryKey: ['wallet', 'jumper', 'quote',
payload?.fromChain, payload?.toChain,
payload?.fromToken, payload?.toToken,
payload?.fromAmount, payload?.fromAddress, payload?.toAddress,
],
queryFn: () => getJumperQuote(payload!),
enabled: !!payload,
staleTime: 10_000,
})
}
export function useFetchJumperQuote() {
return useMutation({ mutationFn: (payload: JumperQuotePayload) => getJumperQuote(payload) })
}
export function useExecuteBridge() {
return useMutation({ mutationFn: (payload: BridgeExecutePayload) => executeBridge(payload) })
}
export function useCreateWallet() {
return useMutation({ mutationFn: createWallet })
}
export function useRevealMnemonic() {
return useQuery({
queryKey: ['wallet', 'mnemonic'],
queryFn: revealMnemonic,
staleTime: Infinity,
retry: false,
})
}
export function useRelayQuote(payload: RelayQuotePayload | null) {
return useQuery({
queryKey: ['relay', 'quote',
payload?.originChainId, payload?.destinationChainId,
payload?.originCurrency, payload?.destinationCurrency, payload?.amount,
],
queryFn: () => getRelayQuote(payload!),
enabled: !!payload,
staleTime: 10_000,
})
}
export function useExecuteRelaySwap() {
return useMutation({
mutationFn: (payload: RelayQuotePayload) => executeRelaySwap(payload),
})
}
export function useSignSwap() {
return useMutation({
mutationFn: ({ chain, txData }: { chain: Chain; txData: unknown }) => {
if (chain === 'SOL') return signSolTx(txData)
return signRawEvmTx(chain as 'ETH' | 'BSC', txData as RelaySwapStep['items'][0]['data'])
},
})
}
export function useTrxSwapQuote(payload: TrxSwapQuotePayload | null) {
return useQuery({
queryKey: ['trx', 'quote', payload?.from, payload?.to, payload?.amountHuman],
queryFn: () => getTrxSwapQuote(payload!),
enabled: !!payload,
staleTime: 10_000,
})
}
export function useFetchTrxQuote() {
return useMutation({ mutationFn: getTrxSwapQuote })
}
export function useExecuteTrxSwap() {
return useMutation({ mutationFn: (quoteId: string) => executeTrxSwap(quoteId) })
}

3106
src/openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export { AdminOrganizationPage } from './ui/AdminOrganizationPage'

View File

@@ -0,0 +1,90 @@
import { useEffect, useState } from 'react'
import { useUpdateOrganization } from '@features/admin'
import type { Organization, UpdateOrganizationRequest } from '@features/admin'
interface FormState {
name: string
short_name: string
ogrn: string
kpp: string
legal_address: string
actual_address: string
contact_person: string
contact_phone: string
status: string
}
function toForm(org: Organization): FormState {
return {
name: org.name ?? '',
short_name: org.short_name ?? '',
ogrn: org.ogrn ?? '',
kpp: org.kpp ?? '',
legal_address: org.legal_address ?? '',
actual_address: org.actual_address ?? '',
contact_person: org.contact_person ?? '',
contact_phone: org.contact_phone ?? '',
status: org.status ?? '',
}
}
function extractErrorMessage(error: unknown): string {
const e = error as { detail?: unknown }
if (typeof e?.detail === 'string') return e.detail
if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) {
return (e.detail[0] as { msg: string }).msg
}
return 'Не удалось сохранить изменения'
}
export function useOrganizationForm(
org: Organization | undefined,
id: string,
onSaved?: () => void,
) {
const [form, setForm] = useState<FormState>(() =>
org ? toForm(org) : {
name: '', short_name: '', ogrn: '', kpp: '', legal_address: '',
actual_address: '', contact_person: '', contact_phone: '', status: '',
},
)
const mutation = useUpdateOrganization(id)
// Sync local form state once the organization loads / changes.
useEffect(() => {
if (org) setForm(toForm(org))
}, [org])
const setField = (key: keyof FormState) => (value: string) =>
setForm((prev) => ({ ...prev, [key]: value }))
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null)
const payload: UpdateOrganizationRequest = {
name: form.name.trim(),
short_name: trimmedOrNull(form.short_name),
ogrn: trimmedOrNull(form.ogrn),
kpp: trimmedOrNull(form.kpp),
legal_address: trimmedOrNull(form.legal_address),
actual_address: trimmedOrNull(form.actual_address),
contact_person: trimmedOrNull(form.contact_person),
contact_phone: trimmedOrNull(form.contact_phone),
status: trimmedOrNull(form.status),
}
mutation.mutate(payload, { onSuccess: () => onSaved?.() })
}
const error = mutation.isError ? extractErrorMessage(mutation.error) : null
return {
form,
setField,
handleSubmit,
isSaving: mutation.isPending,
error,
}
}

View File

@@ -0,0 +1,154 @@
.page {
min-height: 100vh;
background: var(--bg-deep);
padding: 40px 48px;
}
.header {
max-width: 900px;
margin: 0 auto 28px;
}
.back {
background: none;
border: none;
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
font-size: 14px;
cursor: pointer;
padding: 0;
margin-bottom: 14px;
transition: color 0.2s;
}
.back:hover {
color: var(--text-primary, #fff);
}
.title {
font-size: clamp(24px, 3.5vw, 34px);
font-weight: 700;
color: var(--text-primary, #fff);
margin: 0;
}
.form {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.tabs {
max-width: 900px;
margin: 0 auto 24px;
display: flex;
gap: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
font-size: 15px;
font-weight: 600;
padding: 12px 16px;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.tab:hover {
color: var(--text-primary, #fff);
}
.tabActive {
color: var(--text-primary, #fff);
border-bottom-color: var(--interactive, #4a6dff);
}
.tabPanel {
max-width: 900px;
margin: 0 auto;
}
.section {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 24px;
}
.sectionTitle {
font-size: 14px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text-secondary, rgba(255, 255, 255, 0.5));
font-weight: 600;
margin: 0 0 18px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.bankLabel {
display: block;
font-size: 12px;
color: var(--text-secondary, rgba(255, 255, 255, 0.5));
font-weight: 600;
margin-bottom: 8px;
}
.textarea {
width: 100%;
background: var(--glass-bg, rgba(255, 255, 255, 0.06));
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
border-radius: 10px;
color: var(--text-primary, #fff);
font-family: var(--font-mono, monospace);
font-size: 13px;
padding: 12px 14px;
resize: vertical;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.textarea:focus {
border-color: var(--interactive, #4a6dff);
box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15);
}
.state {
max-width: 900px;
margin: 0 auto;
padding: 40px 16px;
text-align: center;
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
}
.error {
color: #ff5a5a;
font-size: 13px;
margin: 0;
text-align: center;
}
.actions {
max-width: 320px;
margin: 0 auto;
width: 100%;
}
@media (max-width: 768px) {
.page {
padding: 28px 20px;
}
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,142 @@
import { useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useAdminAuth, useOrganization } from '@features/admin'
import { ROUTES } from '@shared/config/routes'
import { FormField, Notification, PrimaryButton } from '@shared/ui'
import { AdminLoginForm } from '@widgets/admin-login-form'
import { OrganizationDocuments } from '@widgets/organization-documents'
import { OrganizationPurchaseRequests } from '@widgets/organization-purchase-requests'
import { useOrganizationForm } from '../model/useOrganizationForm'
import styles from './AdminOrganizationPage.module.css'
type Tab = 'info' | 'documents' | 'requests'
const TABS: { id: Tab; label: string }[] = [
{ id: 'info', label: 'Общая информация' },
{ id: 'documents', label: 'Документы' },
{ id: 'requests', label: 'Заявки' },
]
function formatDateTime(value: string | null): string {
if (!value) return '—'
const d = new Date(value)
if (Number.isNaN(d.getTime())) return '—'
return d.toLocaleString('ru-RU')
}
export function AdminOrganizationPage() {
const { isAuthenticated, isLoading: isAuthLoading } = useAdminAuth()
const { organizationId } = useParams<{ organizationId: string }>()
const navigate = useNavigate()
const { data: org, isLoading, isError } = useOrganization(organizationId)
const [notice, setNotice] = useState(false)
const [activeTab, setActiveTab] = useState<Tab>('info')
const { form, setField, handleSubmit, isSaving, error } = useOrganizationForm(
org,
organizationId ?? '',
() => setNotice(true),
)
if (isAuthLoading) return null
if (!isAuthenticated) return <AdminLoginForm />
return (
<div className={styles.page}>
<header className={styles.header}>
<button className={styles.back} type="button" onClick={() => navigate(ROUTES.ADMIN)}>
Назад к списку
</button>
<h1 className={styles.title}>{org ? org.name : 'Юридическое лицо'}</h1>
</header>
{isLoading && <div className={styles.state}>Загрузка...</div>}
{isError && <div className={styles.state}>Не удалось загрузить организацию</div>}
{org && (
<div className={styles.tabs}>
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
className={`${styles.tab} ${activeTab === tab.id ? styles.tabActive : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
)}
{org && activeTab === 'info' && (
<form className={styles.form} onSubmit={handleSubmit}>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Реквизиты</h2>
<div className={styles.grid}>
<FormField label="Наименование" value={form.name} onChange={setField('name')} placeholder="ООО «Ромашка»" required />
<FormField label="Краткое наименование" value={form.short_name} onChange={setField('short_name')} placeholder="Ромашка" />
<FormField label="ИНН" value={org.inn} readOnly icon="lock" />
<FormField label="ОГРН" value={form.ogrn} onChange={setField('ogrn')} placeholder="1027700132195" />
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} placeholder="770801001" />
<FormField label="Статус" value={form.status} onChange={setField('status')} placeholder="active" />
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Адреса</h2>
<div className={styles.grid}>
<FormField label="Юридический адрес" value={form.legal_address} onChange={setField('legal_address')} placeholder="г. Москва, ул. Тверская, д. 1" />
<FormField label="Фактический адрес" value={form.actual_address} onChange={setField('actual_address')} placeholder="г. Москва, ул. Тверская, д. 1" />
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Контакты</h2>
<div className={styles.grid}>
<FormField label="Контактное лицо" value={form.contact_person} onChange={setField('contact_person')} placeholder="Иванов Иван Иванович" />
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} placeholder="+7 (999) 000-00-00" />
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Системная информация</h2>
<div className={styles.grid}>
<FormField label="ID организации" value={org.id} readOnly icon="lock" />
<FormField label="ID пользователя" value={org.user_id} readOnly icon="lock" />
<FormField label="KYC" value={org.kyc_verified ? 'Подтверждён' : 'Не подтверждён'} readOnly />
<FormField label="Дата KYC" value={formatDateTime(org.kyc_verified_at)} readOnly />
<FormField label="Кошельки" value={org.has_wallets ? 'Есть' : 'Нет'} readOnly />
<FormField label="Создано" value={formatDateTime(org.created_at)} readOnly />
<FormField label="Обновлено" value={formatDateTime(org.updated_at)} readOnly />
</div>
</section>
{error && <p className={styles.error}>{error}</p>}
<div className={styles.actions}>
<PrimaryButton label={isSaving ? 'Сохранение...' : 'Сохранить изменения'} disabled={isSaving} />
</div>
</form>
)}
{org && activeTab === 'documents' && (
<div className={styles.tabPanel}>
<OrganizationDocuments orgId={org.id} />
</div>
)}
{org && activeTab === 'requests' && (
<div className={styles.tabPanel}>
<OrganizationPurchaseRequests orgId={org.id} />
</div>
)}
{notice && (
<Notification
status="success"
message="Изменения сохранены"
onClose={() => setNotice(false)}
/>
)}
</div>
)
}

1
src/pages/admin/index.ts Normal file
View File

@@ -0,0 +1 @@
export { AdminPage } from './ui/AdminPage'

View File

@@ -0,0 +1,84 @@
.page {
min-height: 100vh;
background: var(--bg-deep);
padding: 40px 48px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto 36px;
}
.greeting {
font-size: clamp(28px, 4vw, 40px);
font-weight: 700;
color: var(--text-primary, #fff);
margin: 0;
}
.logout {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
color: var(--text-secondary, rgba(255, 255, 255, 0.7));
border-radius: 10px;
height: 40px;
padding: 0 18px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.logout:hover {
background: rgba(255, 90, 90, 0.12);
border-color: rgba(255, 90, 90, 0.3);
color: #ff5a5a;
}
.content {
max-width: 1200px;
margin: 0 auto;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.sectionTitle {
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #fff);
margin: 0;
}
.addBtn {
background: linear-gradient(135deg, var(--grad-edge, #4a6dff), var(--grad-center, #6f4aff));
border: none;
color: #fff;
border-radius: 12px;
height: 44px;
padding: 0 20px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: filter 0.2s, box-shadow 0.2s;
}
.addBtn:hover {
filter: brightness(1.12);
box-shadow: 0 6px 20px rgba(74, 109, 255, 0.35);
}
@media (max-width: 768px) {
.page {
padding: 28px 20px;
}
}

View File

@@ -0,0 +1,77 @@
import { useState } from 'react'
import { useAdminAuth, useAdminLogout, useCreateOrganizationWallets } from '@features/admin'
import type { Organization } from '@features/admin'
import { Notification } from '@shared/ui'
import { AdminLoginForm } from '@widgets/admin-login-form'
import { LegalEntitiesTable } from '@widgets/legal-entities-table'
import { AddLegalEntityModal } from '@widgets/add-legal-entity-modal'
import styles from './AdminPage.module.css'
type NotificationState = { message: string; status: 'success' | 'error' | 'warning' }
export function AdminPage() {
const { isAuthenticated, isLoading } = useAdminAuth()
const logout = useAdminLogout()
const createWallets = useCreateOrganizationWallets()
const [modalOpen, setModalOpen] = useState(false)
const [notification, setNotification] = useState<NotificationState | null>(null)
// After a legal entity is created we immediately provision its wallets.
// The page stays mounted (unlike the modal), so these mutate callbacks fire reliably.
function handleCreated(organization: Organization) {
setNotification({ status: 'success', message: 'Юридическое лицо добавлено' })
createWallets.mutate(organization.id, {
onSuccess: (wallets) => {
setNotification({
status: 'success',
message: `Кошельки созданы (${wallets.length})`,
})
},
onError: () => {
setNotification({
status: 'warning',
message: 'Юридическое лицо создано, но кошельки создать не удалось',
})
},
})
}
if (isLoading) return null
if (!isAuthenticated) return <AdminLoginForm />
return (
<div className={styles.page}>
<header className={styles.header}>
<h1 className={styles.greeting}>Привет, Марк!</h1>
<button className={styles.logout} type="button" onClick={() => logout.mutate()}>
Выйти
</button>
</header>
<section className={styles.content}>
<div className={styles.toolbar}>
<h2 className={styles.sectionTitle}>Юридические лица</h2>
<button className={styles.addBtn} type="button" onClick={() => setModalOpen(true)}>
+ Добавить юридическое лицо
</button>
</div>
<LegalEntitiesTable />
</section>
<AddLegalEntityModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onCreated={handleCreated}
/>
{notification && (
<Notification
status={notification.status}
message={notification.message}
onClose={() => setNotification(null)}
/>
)}
</div>
)
}

View File

@@ -0,0 +1 @@
export { BridgePage } from './ui/BridgePage'

View File

@@ -0,0 +1,12 @@
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 20px 48px;
}
@media (max-width: 650px) {
.content {
padding: 32px 20px;
}
}

View File

@@ -0,0 +1,14 @@
import { BridgeForm } from '@widgets/bridge-form'
import { SwapBridgeTabs } from '@widgets/swap-bridge-tabs'
import styles from './BridgePage.module.css'
export function BridgePage() {
return (
<>
<SwapBridgeTabs active="bridge" />
<div className={styles.content}>
<BridgeForm />
</div>
</>
)
}

View File

@@ -0,0 +1 @@
export { ConverterTestPage } from './ui/ConverterTestPage'

View File

@@ -0,0 +1,133 @@
.page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.wrap {
position: relative;
overflow: hidden;
width: 100%;
max-width: 900px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 40px;
}
.header {
margin-bottom: 40px;
}
.title {
font-size: clamp(32px, 4vw, 48px);
font-weight: 700;
}
.subtitle {
font-size: 15px;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 12px;
max-width: 560px;
}
.body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.formCol {
display: flex;
flex-direction: column;
gap: 20px;
}
.hint {
font-size: 12px;
color: var(--highlight);
margin-top: -12px;
}
/* Панель условий / комиссии */
.infoCol {
display: flex;
flex-direction: column;
}
.infoTitle {
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 24px;
}
.infoRow {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid var(--glass-border);
margin-bottom: 12px;
}
.infoRow[data-accent] {
border-color: var(--grad-center);
background: rgba(91, 61, 184, 0.12);
}
.infoLabel {
font-size: 13px;
color: var(--text-secondary);
}
.infoValue {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 600;
}
.note {
font-size: 12px;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 12px;
}
.submitBtn {
width: 100%;
margin-top: 40px;
padding: 18px;
border-radius: 12px;
background: var(--grad-center);
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
transition: opacity 0.2s;
}
.submitBtn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: 768px) {
.wrap {
padding: 28px 20px;
}
.body {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.header {
margin-bottom: 1.5rem;
}
}

View File

@@ -0,0 +1,105 @@
import { useState } from 'react'
import { FormField } from '@shared/ui'
import styles from './ConverterTestPage.module.css'
const MIN_ORDER = 500_000
const APPROX_RATE = 0.03 // примерная комиссия 3% для крупных заявок
const ru = (n: number) => n.toLocaleString('ru-RU', { maximumFractionDigits: 0 })
export function ConverterTestPage() {
const [amount, setAmount] = useState('')
const [name, setName] = useState('')
const [contact, setContact] = useState('')
const numAmount = Number(amount.replace(/\D/g, '')) || 0
const belowMin = numAmount > 0 && numAmount < MIN_ORDER
const commission = numAmount * APPROX_RATE
const handleAmountChange = (value: string) => {
const digits = value.replace(/\D/g, '')
setAmount(digits ? ru(Number(digits)) : '')
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Тестовая страница — заявка никуда не отправляется.
}
return (
<div className={styles.page}>
<form className={styles.wrap} onSubmit={handleSubmit}>
<div className={styles.header}>
<h1 className={styles.title}>Оставить заявку</h1>
<p className={styles.subtitle}>
Конвертация крупных объёмов по индивидуальному курсу. Оставьте заявку
менеджер свяжется с вами, подтвердит актуальный курс и сопроводит сделку.
</p>
</div>
<div className={styles.body}>
<div className={styles.formCol}>
<FormField
label="Объём заявки, ₽"
type="text"
value={amount}
onChange={handleAmountChange}
placeholder="от 500 000"
/>
{belowMin && (
<p className={styles.hint}>
Минимальный объём заявки {ru(MIN_ORDER)}
</p>
)}
<FormField
label="Как к вам обращаться"
type="text"
value={name}
onChange={setName}
placeholder="Имя"
/>
<FormField
label="Email или телефон для связи"
type="text"
value={contact}
onChange={setContact}
placeholder="example@mail.ru / +7 900 000-00-00"
/>
</div>
<div className={styles.infoCol}>
<div className={styles.infoTitle}>УСЛОВИЯ</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Минимальный объём</span>
<span className={styles.infoValue}>{ru(MIN_ORDER)} </span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Примерная комиссия</span>
<span className={styles.infoValue}>{(APPROX_RATE * 100).toFixed(0)} %</span>
</div>
<div className={styles.infoRow} data-accent>
<span className={styles.infoLabel}>Комиссия с объёма</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${ru(commission)}` : '—'}
</span>
</div>
<p className={styles.note}>
Итоговая комиссия рассчитывается индивидуально и зависит от объёма,
валюты и направления сделки.
</p>
</div>
</div>
<button type="submit" className={styles.submitBtn} disabled={belowMin}>
Оставить заявку
</button>
</form>
</div>
)
}

View File

@@ -1,16 +1,15 @@
import { useMe } from '@features/auth'
import { Spinner } from '@shared/ui'
import { ConverterSection } from '@widgets/converter-page'
import { Footer } from '@widgets/footer'
import { WalletHeader } from '@widgets/wallet-header'
import styles from './ConverterPage.module.css'
import { LegalConverterPage } from './LegalConverterPage'
export function ConverterPage() {
return (
<div className={styles.page}>
<WalletHeader />
<main className={styles.main}>
<ConverterSection />
</main>
<Footer />
</div>
)
const { data, isLoading } = useMe()
if (isLoading) {
return <Spinner fullscreen size="lg" label="Загрузка данных аккаунта" />
}
const isLegal = !!data && data.account_type !== 'individual'
return isLegal ? <LegalConverterPage /> : <ConverterSection />
}

View File

@@ -0,0 +1,125 @@
.wrap {
position: relative;
overflow: hidden;
width: 100%;
max-width: 900px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 40px;
}
.header {
margin-bottom: 40px;
}
.title {
font-size: clamp(32px, 4vw, 48px);
font-weight: 700;
}
.subtitle {
font-size: 15px;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 12px;
max-width: 560px;
}
.body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.formCol {
display: flex;
flex-direction: column;
gap: 20px;
}
.hint {
font-size: 12px;
color: var(--highlight);
margin-top: -12px;
}
/* Панель условий / комиссии */
.infoCol {
display: flex;
flex-direction: column;
}
.infoTitle {
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 24px;
}
.infoRow {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid var(--glass-border);
margin-bottom: 12px;
}
.infoRow[data-accent] {
border-color: var(--grad-center);
background: rgba(91, 61, 184, 0.12);
}
.infoLabel {
font-size: 13px;
color: var(--text-secondary);
}
.infoValue {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 600;
}
.note {
font-size: 12px;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 12px;
}
.submitBtn {
width: 100%;
margin-top: 40px;
padding: 18px;
border-radius: 12px;
background: var(--grad-center);
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
transition: opacity 0.2s;
}
.submitBtn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: 768px) {
.wrap {
padding: 28px 20px;
}
.body {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.header {
margin-bottom: 1.5rem;
}
}

View File

@@ -0,0 +1,153 @@
import { useState } from 'react'
import { FormField, Select } from '@shared/ui'
import styles from './LegalConverterPage.module.css'
const MIN_ORDER = 500_000
// Чем дольше пользователь готов ждать, тем ниже комиссия сервиса.
const TERM_OPTIONS = [
{ days: 3, rate: 0.05 },
{ days: 4, rate: 0.04636 },
{ days: 5, rate: 0.04273 },
{ days: 6, rate: 0.03909 },
{ days: 7, rate: 0.03545 },
{ days: 8, rate: 0.03182 },
{ days: 9, rate: 0.02818 },
{ days: 10, rate: 0.02455 },
{ days: 11, rate: 0.02091 },
{ days: 12, rate: 0.01727 },
{ days: 13, rate: 0.01364 },
{ days: 14, rate: 0.01 },
] as const
const ru = (n: number) => n.toLocaleString('ru-RU', { maximumFractionDigits: 0 })
const dayLabel = (days: number) => {
const mod10 = days % 10
const mod100 = days % 100
if (mod10 === 1 && mod100 !== 11) return `${days} день`
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${days} дня`
return `${days} дней`
}
export function LegalConverterPage() {
const [amount, setAmount] = useState('')
const [name, setName] = useState('')
const [contact, setContact] = useState('')
const [days, setDays] = useState<number>(TERM_OPTIONS[0].days)
const numAmount = Number(amount.replace(/\D/g, '')) || 0
const belowMin = numAmount > 0 && numAmount < MIN_ORDER
const rate = TERM_OPTIONS.find((o) => o.days === days)?.rate ?? TERM_OPTIONS[0].rate
const commission = numAmount * rate
const total = numAmount + commission
const handleAmountChange = (value: string) => {
const digits = value.replace(/\D/g, '')
setAmount(digits ? ru(Number(digits)) : '')
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Бэкенд пока не подключён — заявка никуда не отправляется.
}
return (
<form className={styles.wrap} onSubmit={handleSubmit}>
<div className={styles.header}>
<h1 className={styles.title}>Оставить заявку</h1>
<p className={styles.subtitle}>
Конвертация крупных объёмов по индивидуальному курсу. Оставьте заявку
менеджер свяжется с вами, подтвердит актуальный курс и сопроводит сделку.
</p>
</div>
<div className={styles.body}>
<div className={styles.formCol}>
<FormField
label="Объём заявки, ₽"
type="text"
value={amount}
onChange={handleAmountChange}
placeholder="от 500 000"
/>
{belowMin && (
<p className={styles.hint}>
Минимальный объём заявки {ru(MIN_ORDER)}
</p>
)}
<Select
id="term"
label="Срок ожидания операции"
value={days}
onChange={setDays}
options={TERM_OPTIONS.map((o) => ({
value: o.days,
label: `${dayLabel(o.days)} — комиссия ${(o.rate * 100).toFixed(3)} %`,
}))}
/>
<FormField
label="Как к вам обращаться"
type="text"
value={name}
onChange={setName}
placeholder="Имя"
/>
<FormField
label="Email или телефон для связи"
type="text"
value={contact}
onChange={setContact}
placeholder="example@mail.ru / +7 900 000-00-00"
/>
</div>
<div className={styles.infoCol}>
<div className={styles.infoTitle}>УСЛОВИЯ</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Минимальный объём</span>
<span className={styles.infoValue}>{ru(MIN_ORDER)} </span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Срок ожидания</span>
<span className={styles.infoValue}>{dayLabel(days)}</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Ставка комиссии</span>
<span className={styles.infoValue}>{(rate * 100).toFixed(3)} %</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Сумма комиссии</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${ru(commission)}` : '—'}
</span>
</div>
<div className={styles.infoRow} data-accent>
<span className={styles.infoLabel}>Итого к оплате</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${ru(total)}` : '—'}
</span>
</div>
<p className={styles.note}>
Итоговая комиссия рассчитывается индивидуально и зависит от объёма,
валюты и направления сделки.
</p>
</div>
</div>
<button type="submit" className={styles.submitBtn} disabled={belowMin}>
Оставить заявку
</button>
</form>
)
}

View File

@@ -1,7 +1,15 @@
import { Navigate } from 'react-router-dom'
import { useMe } from '@features/auth'
import { ROUTES } from '@shared/config/routes'
import { KycWidget } from '@widgets/kyc-verification'
import styles from './KycPage.module.css'
export function KycPage() {
const { data, isLoading } = useMe()
if (isLoading) return null
if (data?.kyc_verified) return <Navigate to={ROUTES.PROFILE} replace />
return (
<div className={styles.page}>
<KycWidget />

View File

@@ -0,0 +1 @@
export { PolitikaCookiePage } from './ui/PolitikaCookiePage'

View File

@@ -0,0 +1,115 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 30px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.subSectionTitle {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.list strong {
color: var(--text-primary, #ffffff);
}
.info {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
margin: 15px 0;
line-height: 1.8;
}
.info p {
margin: 5px 0;
color: var(--text-primary, #ffffff);
}
.example {
padding: 10px 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 3px solid var(--interactive, #4a6dff);
border-radius: 4px;
font-style: italic;
color: var(--text-primary, #ffffff);
margin: 10px 0;
}
.warning {
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
color: var(--text-primary, #ffffff);
margin: 15px 0;
font-weight: 500;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.sectionTitle {
font-size: 16px;
}
.subSectionTitle {
font-size: 14px;
}
.list {
margin-left: 15px;
}
.info {
padding: 15px;
}
}

View File

@@ -0,0 +1,274 @@
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PolitikaCookiePage.module.css'
export function PolitikaCookiePage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПОЛИТИКА ИСПОЛЬЗОВАНИЯ ФАЙЛОВ COOKIE</h1>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Общие положения и терминология</h3>
<p>
Настоящая Политика использования файлов cookie устанавливает порядок обработки файлов cookie и содержащихся в них персональных данных ООО «БИТФОРС» при использовании пользователями интернет-ресурса https://bitforce-foundation.ru.
</p>
<p>
Файлы cookie это текстовые файлы небольшого размера, которые устанавливаются на пользовательское устройство при посещении интернет-ресурса или совершении на нем определенных действий. Файлы cookie остаются сохраненными на устройстве даже после покидания ресурса, что позволяет «узнавать» пользователя при последующих посещениях.
</p>
<p>
К персональным данным относится не сам файл cookie, а его содержимое уникальные идентификаторы, IP-адреса, информация о предпочтениях пользователя и другие данные, позволяющие прямо или косвенно идентифицировать физическое лицо.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Оператор персональных данных</h3>
<p>Оператором персональных данных, содержащихся в файлах cookie, является:</p>
<div className={styles.info}>
<p>ООО «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н</p>
</div>
<p>
Оператор определяет цели обработки персональных данных, их состав, а также действия с персональными данными, включая случаи использования сторонних файлов cookie.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Категории файлов cookie и их назначение</h3>
<h4 className={styles.subSectionTitle}>1. Строго необходимые (технические) файлы cookie</h4>
<p>
Данные файлы обеспечивают работу интернет-ресурса и предоставление необходимого уровня сервиса: авторизацию, навигацию, отображение контента в соответствии с параметрами устройства, обеспечение безопасности.
</p>
<p>
Обработка таких файлов cookie осуществляется на основании п. 5 ч. 1 ст. 6 ФЗ 152 (заключение и исполнение договора). Согласие на использование строго необходимых файлов cookie не требуется.
</p>
<p className={styles.example}>Примеры: файлы сессий (PHPSESSID), настройки безопасности, файлы аутентификации.</p>
<h4 className={styles.subSectionTitle}>2. Функциональные файлы cookie</h4>
<p>
Используются для запоминания пользовательских предпочтений и персонализации взаимодействия с сайтом: сохранение выбранного языка, региона, настроек отображения, размера шрифта.
</p>
<p>
Обработка осуществляется на основании согласия субъекта персональных данных, поскольку данная обработка не является строго необходимой для функционирования сайта.
</p>
<p className={styles.example}>Примеры: настройки языка интерфейса, предпочтения отображения, настройки доступности.</p>
<h4 className={styles.subSectionTitle}>3. Аналитические файлы cookie</h4>
<p>
Собирают информацию о взаимодействии пользователей с интернет-ресурсом для анализа его использования, выявления популярных разделов, обнаружения ошибок и улучшения пользовательского опыта. Могут содержать персональные данные, включая IP-адреса пользователей.
</p>
<p>
Обработка осуществляется на основании согласия субъекта персональных данных.
</p>
<h4 className={styles.subSectionTitle}>4. Маркетинговые файлы cookie</h4>
<p>
Используются для отслеживания пользователей в целях персонализированной рекламы, анализа эффективности рекламных кампаний, ретаргетинга.
</p>
<p>
Обработка осуществляется исключительно на основании согласия субъекта персональных данных.
</p>
<p className={styles.example}>Примеры: пиксели социальных сетей, рекламные идентификаторы, файлы ретаргетинга.</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Правовые основания обработки персональных данных</h3>
<p>Обработка персональных данных, содержащихся в файлах cookie, осуществляется на следующих правовых основаниях:</p>
<ul className={styles.list}>
<li>
<strong>Согласие субъекта персональных данных</strong> для функциональных, аналитических и маркетинговых файлов cookie
</li>
<li>
<strong>Заключение и исполнение договора</strong> для строго необходимых файлов cookie, обеспечивающих работу интернет-ресурса
</li>
<li>
<strong>Законные интересы оператора</strong> в исключительных случаях, когда отсутствуют иные основания
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Порядок получения согласия</h3>
<h4 className={styles.subSectionTitle}>Принципы получения согласия:</h4>
<ul className={styles.list}>
<li>Согласие должно быть получено до начала обработки персональных данных</li>
<li>Информация об использовании файлов cookie размещается на первом уровне интернет-ресурса</li>
<li>Предоставляется возможность выбора категорий файлов cookie</li>
<li>Используются активные формулировки вместо пассивных</li>
</ul>
<h4 className={styles.subSectionTitle}>Критерии действительного согласия:</h4>
<ul className={styles.list}>
<li>
<strong>Добровольность</strong> согласие дается по свободной воле субъекта
</li>
<li>
<strong>Конкретность</strong> четко определены цели обработки
</li>
<li>
<strong>Информированность</strong> предоставлена полная информация об обработке
</li>
<li>
<strong>Однозначность</strong> согласие выражено в недвусмысленной форме
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Сторонние файлы cookie</h3>
<h4 className={styles.subSectionTitle}>Использование сторонних сервисов:</h4>
<p>Наш интернет-ресурс использует файлы cookie сторонних сервисов, включая:</p>
<ul className={styles.list}>
<li>Яндекс.Метрика (ООО «ЯНДЕКС», Россия)</li>
<li>Социальные сети и сервисы интеграции</li>
</ul>
<h4 className={styles.subSectionTitle}>Обеспечение защиты:</h4>
<ul className={styles.list}>
<li>Получено согласие на передачу</li>
<li>Применяются дополнительные меры защиты данных</li>
<li>Контролируется соблюдение принципов обработки персональных данных получателями</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Сроки обработки и хранения</h3>
<h4 className={styles.subSectionTitle}>Категории по срокам хранения:</h4>
<ul className={styles.list}>
<li>Сеансовые cookie удаляются автоматически при закрытии браузера</li>
<li>Постоянные cookie хранятся установленный период или до удаления пользователем</li>
</ul>
<h4 className={styles.subSectionTitle}>Конкретные сроки:</h4>
<ul className={styles.list}>
<li>Необходимые файлы cookie до 12 месяцев</li>
<li>Функциональные файлы cookie до 12 месяцев</li>
<li>Аналитические файлы cookie до 24 месяцев</li>
<li>Маркетинговые файлы cookie до 24 месяцев</li>
</ul>
<p>
По истечении установленных сроков файлы cookie удаляются автоматически. Пользователь может удалить файлы cookie досрочно через настройки браузера или отозвать согласие на их обработку.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Права субъектов персональных данных</h3>
<h4 className={styles.subSectionTitle}>Право на информацию:</h4>
<ul className={styles.list}>
<li>Получение информации о обработке персональных данных</li>
<li>Сведения о правовых основаниях и целях обработки</li>
<li>Информация о сроках обработки и составе данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на доступ:</h4>
<ul className={styles.list}>
<li>Получение подтверждения факта обработки</li>
<li>Ознакомление с обрабатываемыми персональными данными</li>
<li>Получение информации об источниках персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на уточнение, блокирование, удаление:</h4>
<ul className={styles.list}>
<li>Требование уточнения неточных данных</li>
<li>Блокирование недостоверных данных</li>
<li>Удаление незаконно полученных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на отзыв согласия:</h4>
<ul className={styles.list}>
<li>Отзыв согласия в любое время</li>
<li>Прекращение обработки после отзыва согласия</li>
<li>Сохранение права на обжалование действий оператора</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Способы управления файлами cookie</h3>
<h4 className={styles.subSectionTitle}>Управление через настройки сайта:</h4>
<ul className={styles.list}>
<li>Использование баннера согласия на файлы cookie</li>
<li>Изменение настроек в любое время через интерфейс сайта</li>
<li>Отзыв согласия на использование отдельных категорий файлов cookie</li>
</ul>
<h4 className={styles.subSectionTitle}>Управление через браузер:</h4>
<p>Большинство браузеров позволяют контролировать файлы cookie:</p>
<ul className={styles.list}>
<li>Блокировка запрет установки новых файлов cookie</li>
<li>Удаление очистка существующих файлов cookie</li>
<li>Уведомления получение предупреждений при установке файлов cookie</li>
<li>Селективная настройка разрешение файлов cookie только для определенных сайтов</li>
</ul>
<h4 className={styles.subSectionTitle}>Инструкции для популярных браузеров:</h4>
<ul className={styles.list}>
<li>Google Chrome: Настройки Конфиденциальность и безопасность Файлы cookie</li>
<li>Mozilla Firefox: Настройки Приватность и Защита Файлы cookie</li>
<li>Safari: Настройки Конфиденциальность Файлы cookie</li>
<li>Microsoft Edge: Настройки Файлы cookie и разрешения сайтов</li>
</ul>
<p className={styles.warning}>
Блокировка необходимых файлов cookie может привести к ограничению функциональности интернет-ресурса.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Меры безопасности</h3>
<p>
Оператор применяет правовые, организационные и технические меры для защиты персональных данных:
</p>
<h4 className={styles.subSectionTitle}>Правовые меры:</h4>
<ul className={styles.list}>
<li>Назначение ответственного за организацию обработки персональных данных</li>
<li>Ознакомление сотрудников с требованиями законодательства</li>
<li>Заключение соглашений о неразглашении персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Организационные меры:</h4>
<ul className={styles.list}>
<li>Определение перечня лиц, допущенных к обработке персональных данных</li>
<li>Установление правил доступа к персональным данным</li>
<li>Контроль за соблюдением требований по защите персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Технические меры:</h4>
<ul className={styles.list}>
<li>Использование средств защиты информации</li>
<li>Применение криптографических средств защиты</li>
<li>Обеспечение целостности и доступности персональных данных</li>
<li>Регулярное обновление систем защиты информации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Контактная информация и обращения</h3>
<p>Для реализации прав субъекта персональных данных обращайтесь к нам:</p>
<div className={styles.info}>
<p>ООО «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н</p>
<p>Email компании: company@bitforcefoundation.ru</p>
</div>
<h4 className={styles.subSectionTitle}>Порядок рассмотрения обращений:</h4>
<ul className={styles.list}>
<li>Срок рассмотрения обращений до 30 дней с момента получения</li>
<li>Обращения рассматриваются в письменной форме</li>
<li>Ответ направляется способом, указанным в обращении</li>
<li>При отказе в удовлетворении требований указываются мотивированные основания</li>
</ul>
</section>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1 @@
export { PolitikaPage } from './ui/PolitikaPage'

View File

@@ -0,0 +1,138 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.subSectionTitle {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.definitions {
display: flex;
flex-direction: column;
gap: 15px;
}
.definition {
padding: 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.6;
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.goalsList {
display: flex;
flex-direction: column;
gap: 20px;
}
.goal {
padding: 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
}
.goal strong {
display: block;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.goal ul {
list-style: disc;
margin-left: 20px;
line-height: 1.6;
}
.goal li {
margin-bottom: 6px;
color: var(--text-primary, #ffffff);
}
.contacts {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 4px;
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.sectionTitle {
font-size: 16px;
}
.subSectionTitle {
font-size: 14px;
}
.list {
margin-left: 15px;
}
}

View File

@@ -0,0 +1,300 @@
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PolitikaPage.module.css'
export function PolitikaPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Общие положения</h3>
<p>
Настоящая Политика обработки персональных данных разработана в соответствии с Федеральным законом от 27.07.2006 152-ФЗ «О персональных данных» и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые ООО «БИТФОРС».
</p>
<p>
Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты права на неприкосновенность частной жизни, личную и семейную тайну.
</p>
<p>
Настоящая Политика действует в отношении всех персональных данных, которые обрабатываются Оператором с использованием средств автоматизации и без использования таких средств.
</p>
<h4 className={styles.subSectionTitle}>1.4. Основные понятия</h4>
<div className={styles.definitions}>
<div className={styles.definition}>
<strong>Автоматизированная обработка персональных данных</strong> обработка персональных данных с помощью средств вычислительной техники.
</div>
<div className={styles.definition}>
<strong>Обработка персональных данных</strong> любое действие или совокупность действий, совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение, извлечение, использование, передачу, обезличивание, блокирование, удаление, уничтожение.
</div>
<div className={styles.definition}>
<strong>Оператор</strong> юридическое или физическое лицо, организующие и осуществляющие обработку персональных данных.
</div>
<div className={styles.definition}>
<strong>Персональные данные</strong> любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу.
</div>
<div className={styles.definition}>
<strong>Пользователь</strong> любой посетитель веб-сайта https://bitforce-foundation.ru.
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Сведения об операторе</h3>
<ul className={styles.list}>
<li>Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»</li>
<li>Сокращенное наименование: ООО «БИТФОРС»</li>
<li>ИНН: 9810001062</li>
<li>ОГРН: 1257800060990</li>
<li>Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н</li>
<li>Электронная почта: company@bitforcefoundation.ru</li>
<li>Веб-сайт: https://bitforce-foundation.ru</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Общие цели обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>3.1.1. Основная деятельность:</h4>
<ul className={styles.list}>
<li>Предоставление услуг по конвертации иного имущества</li>
<li>Осуществление операций на криптовалютных рынках</li>
<li>Предоставление услуг в области блокчейн технологий</li>
<li>Обеспечение функционирования интернет-платформы и мобильных приложений</li>
</ul>
<h4 className={styles.subSectionTitle}>3.1.2. Обеспечение безопасности:</h4>
<ul className={styles.list}>
<li>Предотвращение мошенничества и отмывания денежных средств</li>
<li>Обеспечение безопасности платежных операций</li>
<li>Выполнение требований по противодействию легализации доходов</li>
<li>Идентификация и верификация клиентов</li>
</ul>
<h4 className={styles.subSectionTitle}>3.1.3. Соблюдение законодательства:</h4>
<ul className={styles.list}>
<li>Исполнение требований российского и международного законодательства</li>
<li>Взаимодействие с контролирующими и правоохранительными органами</li>
<li>Ведение обязательной отчетности и документооборота</li>
<li>Соблюдение налогового законодательства</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Цели сбора персональных данных</h3>
<div className={styles.goalsList}>
<div className={styles.goal}>
<strong>Регистрация и идентификация пользователей:</strong>
<ul>
<li>Создание учетной записи на веб-сайте</li>
<li>Верификация личности в соответствии с требованиями законодательства</li>
<li>Подтверждение права на осуществление операций</li>
</ul>
</div>
<div className={styles.goal}>
<strong>Обработка платежей и финансовых операций:</strong>
<ul>
<li>Осуществление операций по конвертации криптовалют</li>
<li>Проведение расчетов и переводов денежных средств</li>
<li>Ведение учета и истории транзакций</li>
</ul>
</div>
<div className={styles.goal}>
<strong>Коммуникация с клиентами:</strong>
<ul>
<li>Предоставление технической поддержки</li>
<li>Уведомления о состоянии операций и счетов</li>
<li>Информирование об изменениях в условиях предоставления услуг</li>
</ul>
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Правовые основания обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>5.1.1. Согласие субъекта персональных данных:</h4>
<ul className={styles.list}>
<li>Обработка персональных данных в маркетинговых целях</li>
<li>Использование файлов cookie и метрик</li>
<li>Персонализация сервисов и предложений</li>
</ul>
<h4 className={styles.subSectionTitle}>5.1.2. Необходимость исполнения договора:</h4>
<ul className={styles.list}>
<li>Регистрация и ведение учетных записей пользователей</li>
<li>Осуществление финансовых операций и переводов</li>
<li>Предоставление доступа к платформе и сервисам</li>
<li>Техническая поддержка и обслуживание клиентов</li>
</ul>
<h4 className={styles.subSectionTitle}>5.1.3. Соблюдение правовой обязанности:</h4>
<ul className={styles.list}>
<li>Выполнение требований валютного законодательства</li>
<li>Противодействие легализации доходов, полученных преступным путем</li>
<li>Соблюдение требований по налоговому учету и отчетности</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Объем и категории обрабатываемых персональных данных</h3>
<h4 className={styles.subSectionTitle}>6.1.1. Пользователи веб-сайта и мобильного приложения:</h4>
<ul className={styles.list}>
<li>Зарегистрированные пользователи</li>
<li>Посетители сайта без регистрации</li>
<li>Потенциальные клиенты</li>
<li>Бывшие клиенты</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.1. Идентификационные данные:</h4>
<ul className={styles.list}>
<li>Фамилия, имя, отчество</li>
<li>Дата рождения</li>
<li>Гражданство</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.3. Контактная информация:</h4>
<ul className={styles.list}>
<li>Номера телефонов (мобильный, домашний, рабочий)</li>
<li>Адреса электронной почты</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.4. Финансовая информация:</h4>
<ul className={styles.list}>
<li>Номера банковских счетов и карт</li>
<li>Реквизиты кошельков криптовалют</li>
<li>История операций и транзакций</li>
<li>Данные о доходах и источниках средств</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.5. Техническая информация:</h4>
<ul className={styles.list}>
<li>IP-адреса устройств</li>
<li>Данные о браузере и операционной системе</li>
<li>Файлы cookie и локальное хранилище</li>
<li>Логи действий на сайте</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Порядок и условия обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>7.1. Принципы обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Обработка осуществляется на законной и справедливой основе</li>
<li>Обработка ограничивается достижением конкретных, заранее определенных целей</li>
<li>Содержание и объем данных соответствуют заявленным целям</li>
<li>Обрабатываемые персональные данные являются точными и актуальными</li>
</ul>
<h4 className={styles.subSectionTitle}>7.4. Сроки обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Персональные данные обрабатываются в течение времени, необходимого для достижения целей</li>
<li>После достижения целей персональные данные подлежат уничтожению или обезличиванию</li>
<li>Сроки хранения определяются требованиями законодательства</li>
</ul>
<h4 className={styles.subSectionTitle}>7.5. Места обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Основные серверы и хранилища данных расположены на территории Российской Федерации</li>
<li>Резервные копии могут храниться в дата-центрах на территории РФ</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Актуализация, исправление, удаление и уничтожение персональных данных</h3>
<h4 className={styles.subSectionTitle}>8.2.2. Процедура исправления:</h4>
<ul className={styles.list}>
<li>Рассмотрение запроса в течение 30 дней</li>
<li>Проверка обоснованности требования об исправлении</li>
<li>Внесение изменений во все информационные системы</li>
<li>Уведомление субъекта о проведенных исправлениях</li>
</ul>
<h4 className={styles.subSectionTitle}>8.3.2. Процедура удаления:</h4>
<ul className={styles.list}>
<li>Проверка наличия законных оснований для продолжения обработки</li>
<li>Удаление из всех информационных систем и баз данных</li>
<li>Удаление резервных копий (кроме архивных)</li>
<li>Уведомление субъекта о выполненном удалении</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Ответы на запросы субъектов персональных данных</h3>
<h4 className={styles.subSectionTitle}>9.1.1. Право на информацию:</h4>
<ul className={styles.list}>
<li>Подтверждение факта обработки персональных данных</li>
<li>Правовые основания и цели обработки</li>
<li>Применяемые способы обработки</li>
<li>Наименование и местонахождение оператора</li>
<li>Лица, имеющие доступ к персональным данным</li>
</ul>
<h4 className={styles.subSectionTitle}>9.2.2. Сроки рассмотрения:</h4>
<ul className={styles.list}>
<li>Срок рассмотрения запроса составляет 30 дней с момента получения</li>
<li>Срок может быть продлен на 30 дней при большом объеме информации</li>
<li>О продлении срока субъект уведомляется в течение 30 дней</li>
</ul>
<h4 className={styles.subSectionTitle}>9.4. Плата за предоставление информации:</h4>
<ul className={styles.list}>
<li>Первый запрос в течение года обрабатывается бесплатно</li>
<li>За повторные запросы может взиматься плата в размере расходов</li>
<li>Субъект уведомляется о размере платы до предоставления информации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>10. Обеспечение безопасности персональных данных</h3>
<h4 className={styles.subSectionTitle}>10.1. Правовые меры:</h4>
<ul className={styles.list}>
<li>Назначение ответственного за организацию обработки персональных данных</li>
<li>Принятие локальных актов по вопросам обработки персональных данных</li>
<li>Ознакомление работников с требованиями законодательства</li>
<li>Применение мер ответственности за нарушение требований</li>
</ul>
<h4 className={styles.subSectionTitle}>10.3. Технические меры:</h4>
<ul className={styles.list}>
<li>Предотвращение несанкционированного доступа к персональным данным</li>
<li>Своевременное обнаружение фактов несанкционированного доступа</li>
<li>Возможность незамедлительного восстановления персональных данных</li>
<li>Постоянный контроль за обеспечением уровня защищенности</li>
</ul>
<h4 className={styles.subSectionTitle}>10.4. Конкретные технические решения:</h4>
<ul className={styles.list}>
<li>Использование сертифицированных средств защиты информации</li>
<li>Шифрование персональных данных при передаче и хранении</li>
<li>Применение межсетевых экранов и систем обнаружения вторжений</li>
<li>Резервное копирование и обеспечение отказоустойчивости</li>
<li>Антивирусная защита и обновление программного обеспечения</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>12. Заключительные положения</h3>
<h4 className={styles.subSectionTitle}>12.2. Жалобы и обращения:</h4>
<ul className={styles.list}>
<li>Субъекты персональных данных могут обратиться к Оператору по вопросам обработки</li>
<li>Жалобы рассматриваются в установленном законом порядке</li>
<li>При неурегулировании разногласий возможно обращение в Роскомнадзор или суд</li>
</ul>
<h4 className={styles.subSectionTitle}>12.4. Контактная информация для обращений:</h4>
<p className={styles.contacts}>
Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н<br />
Электронная почта: company@bitforcefoundation.ru
</p>
</section>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1,34 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
interface Props {
data: MeResponse
fullName: string
phone: string
onPhoneChange: (value: string) => void
onPhoneBlur: () => void
}
export function IndividualFields({ data, fullName, phone, onPhoneChange, onPhoneBlur }: Props) {
return (
<>
<ProfileSection title="Личные данные">
<div className={styles.grid2}>
<FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" readOnly />
<FormField label="Адрес электронной почты" value={data.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly />
<FormField label="Серия и номер паспорта" value={data.passport_data ?? ''} placeholder="0000 000000" readOnly />
<FormField label="Номер телефона" value={phone} onChange={onPhoneChange} onBlur={onPhoneBlur} type="tel" placeholder="+7 (999) 000-00-00" />
</div>
</ProfileSection>
<ProfileSection title="Верификация">
<div className={styles.grid2}>
<FormField label="ИНН" value={data.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ID аккаунта" value={data.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
</>
)
}

View File

@@ -0,0 +1,47 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
interface Props {
data: MeResponse
}
// Legal-account fields. Organization data lives in the nested `legal_entity`
// object; person-level fields are null on these accounts.
// All read-only — organization data is managed admin-side, not by the user.
export function LegalEntityFields({ data }: Props) {
const le = data.legal_entity
if (!le) return null
return (
<>
<ProfileSection title="Данные организации">
<div className={styles.grid2}>
<FormField label="Наименование" value={le.name ?? ''} placeholder="ООО «Ромашка»" readOnly />
<FormField label="Краткое наименование" value={le.short_name ?? ''} placeholder="Ромашка" readOnly />
<FormField label="ИНН" value={le.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ОГРН" value={le.ogrn ?? ''} placeholder="1027700132195" readOnly />
<FormField label="КПП" value={le.kpp ?? ''} placeholder="770801001" readOnly />
<FormField label="Адрес электронной почты" value={data.email ?? ''} type="email" icon="check" placeholder="org@mail.ru" readOnly />
</div>
</ProfileSection>
<ProfileSection title="Адреса">
<div className={styles.grid2}>
<FormField label="Юридический адрес" value={le.legal_address ?? ''} placeholder="г. Москва, ул. Тверская, д. 1" readOnly />
<FormField label="Фактический адрес" value={le.actual_address ?? ''} placeholder="г. Москва, ул. Тверская, д. 1" readOnly />
</div>
</ProfileSection>
<ProfileSection title="Контакты и верификация">
<div className={styles.grid2}>
<FormField label="Контактное лицо" value={le.contact_person ?? ''} placeholder="Иванов Иван Иванович" readOnly />
<FormField label="Контактный телефон" value={le.contact_phone ?? ''} type="tel" placeholder="+7 (999) 000-00-00" readOnly />
<FormField label="Статус" value={le.status ?? ''} placeholder="active" readOnly />
<FormField label="ID аккаунта" value={data.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
</>
)
}

View File

@@ -1,16 +1,64 @@
import { useMe } from '@features/auth'
import { Button, FormField } from '@shared/ui'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useMe, useUpdatePhone } from '@features/auth'
import { usePortfolio, useWalletAddresses } from '@features/wallet'
import { ROUTES } from '@shared/config/routes'
import { Button, FormField, Notification } from '@shared/ui'
import { WalletHeader } from '@widgets/wallet-header'
import { ProfileAvatar, ProfileSection } from '@widgets/profile'
import { IndividualFields } from './IndividualFields'
import { LegalEntityFields } from './LegalEntityFields'
import styles from './ProfilePage.module.css'
export function ProfilePage() {
const { data } = useMe()
const { data: portfolio, isLoading: isPortfolioLoading } = usePortfolio()
const { data: walletAddresses } = useWalletAddresses()
const updatePhone = useUpdatePhone()
const navigate = useNavigate()
const [phone, setPhone] = useState('')
const [savedPhone, setSavedPhone] = useState('')
const [notification, setNotification] = useState<{ message: string; status: 'success' | 'error' } | null>(null)
useEffect(() => {
if (data?.phone != null) {
setPhone(data.phone)
setSavedPhone(data.phone)
}
}, [data?.phone])
function handlePhoneChange(value: string) {
setPhone(value.replace(/[^\d+\s()-]/g, ''))
}
function handlePhoneBlur() {
const next = phone.trim()
if (next === savedPhone || updatePhone.isPending) return
updatePhone.mutate(next, {
onSuccess: () => {
setSavedPhone(next)
setNotification({ status: 'success', message: 'Номер телефона обновлён' })
},
onError: () => {
setNotification({ status: 'error', message: 'Не удалось обновить номер телефона' })
},
})
}
const capitalize = (s: string | null) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const fullName = data
? [data.last_name, data.first_name, data.middle_name].filter(Boolean).join(' ')
? [data.last_name, data.first_name, data.middle_name].filter(Boolean).map(capitalize).join(' ')
: ''
const isLegal = !!data && data.account_type !== 'individual'
const displayName = isLegal ? (data?.legal_entity?.name ?? '') : fullName
const userBalance =
isPortfolioLoading || !portfolio || portfolio.totalUsd == null
? '$—'
: `$${portfolio.totalUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
return (
<div className={styles.page}>
<WalletHeader />
@@ -23,28 +71,24 @@ export function ProfilePage() {
<div className={styles.profileTop}>
<ProfileAvatar />
<div className={styles.userInfo}>
<span className={styles.userName}>{fullName}</span>
<span className={styles.userBalance}>$245.00</span>
<span className={styles.userBalanceRub}> 22 340,50 </span>
<span className={styles.userName}>{displayName}</span>
<span className={styles.userBalance}>{userBalance}</span>
{/* <span className={styles.userBalanceRub}>≈ 22 340,50 ₽</span> */}
</div>
</div>
<div className={styles.sections}>
<ProfileSection title="Личные данные">
<div className={styles.grid2}>
<FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" />
<FormField label="Адрес электронной почты" value={data?.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly />
<FormField label="Серия и номер паспорта" value={data?.passport_data ?? ''} placeholder="0000 000000" readOnly />
<FormField label="Номер телефона" value={data?.phone ?? ''} type="tel" icon="check" placeholder="+7 (999) 000-00-00" readOnly />
</div>
</ProfileSection>
<ProfileSection title="Верификация">
<div className={styles.grid2}>
<FormField label="ИНН" value={data?.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ID аккаунта" value={data?.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
{data && (isLegal ? (
<LegalEntityFields data={data} />
) : (
<IndividualFields
data={data}
fullName={fullName}
phone={phone}
onPhoneChange={handlePhoneChange}
onPhoneBlur={handlePhoneBlur}
/>
))}
<ProfileSection
title="Безопасность"
@@ -56,7 +100,16 @@ export function ProfilePage() {
}
>
<div className={styles.grid1}>
<FormField label="Адрес ERC-20" readOnly icon="lock" value={data?.erc20 ?? ''} placeholder="0x0000000000000000000000000000000000000000" />
{walletAddresses?.map(({ chain, address }) => (
<FormField
key={chain}
label={`Адрес ${chain}`}
readOnly
icon="lock"
value={address}
placeholder="—"
/>
))}
</div>
</ProfileSection>
@@ -66,11 +119,18 @@ export function ProfilePage() {
<span className={styles.mnemonicIcon}>🔑</span>
<span className={styles.mnemonicText}>Сид-фраза из 12 слов для восстановления кошелька</span>
</div>
<Button variant="danger"> Показать мнемонику</Button>
<Button variant="danger" onClick={() => navigate(ROUTES.SEED_PHRASE)}> Показать мнемонику</Button>
</div>
</ProfileSection>
</div>
</main>
{notification && (
<Notification
status={notification.status}
message={notification.message}
onClose={() => setNotification(null)}
/>
)}
</div>
)
}

View File

@@ -0,0 +1 @@
export { PublichnayaOfertaPage } from './ui/PublichnayaOfertaPage'

View File

@@ -0,0 +1,89 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.definitions {
display: flex;
flex-direction: column;
gap: 15px;
}
.definition {
padding: 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.6;
}
.requisites {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 4px;
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
line-height: 1.8;
}
.requisites p {
margin: 5px 0;
color: var(--text-primary, #ffffff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.sectionTitle {
font-size: 16px;
}
.requisites {
padding: 15px;
}
}

View File

@@ -0,0 +1,200 @@
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PublichnayaOfertaPage.module.css'
export function PublichnayaOfertaPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПУБЛИЧНЫЙ ДОГОВОР ОФЕРТЫ</h1>
<h2 className={styles.subtitle}>ООО БИТФОРС</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Агентский договор</h3>
<p>
Настоящая оферта на заключение агентского договора (далее Оферта, Договор) является публичным предложением Общества с ограниченной ответственностью «БИТФОРС», заключить договор на условиях и в порядке, определенных настоящей Офертой.
</p>
<p>
Акцепт оферты производится в соответствии с пунктом 2 статьи 437 Гражданского кодекса Российской Федерации и равносилен заключению агентского договора в письменной форме.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Основные понятия и определения действующего договора</h3>
<div className={styles.definitions}>
<div className={styles.definition}>
<strong>Агент</strong> юридическое лицо или индивидуальный предприниматель, зарегистрированный на территории Российской Федерации, в установленном действующим законодательством порядке.
</div>
<div className={styles.definition}>
<strong>Принципал</strong> сторона агентского договора, по поручению которой агент осуществляет юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала.
</div>
<div className={styles.definition}>
<strong>Агентский договор</strong> соглашение, по которому агент обязуется за вознаграждение совершать по поручению принципала юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала в соответствии с п. 1 ст. 1005 Гражданского Кодекса Российской Федерации.
</div>
<div className={styles.definition}>
<strong>Личный кабинета Агента</strong> ресурс, размещенный на сайте Принципала, предназначенный для взаимодействия Агента и Принципала.
</div>
<div className={styles.definition}>
<strong>Отчетный период</strong> период для взаиморасчетов с Агентом, равный одному календарному кварталу с даты активации любой из услуг, предоставляемой Принципалу.
</div>
<div className={styles.definition}>
<strong>Отчет о сумме начислений (Отчет)</strong> отчет, формируемый в Личном кабинете Агента на основании данных систем учета Принципала.
</div>
<div className={styles.definition}>
<strong>Оферта (Договор)</strong> настоящий документ, который отражает предложение и намерение ООО «БИТФОРС» считать заключенным договор с лицом, которым будет принято предложение на условиях, изложенных ниже.
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Акцепт оферты и заключение агентского договора</h3>
<p>
Акцепт настоящей Оферты и заключение Агентского договора осуществляется Принципалом в процессе регистрации в Личном кабинете Принципала (на сайте Агента), при прочтении текста настоящей Оферты, путем проставления специальной отметки (галочки) напротив фразы «Я ознакомился с Офертой и принимаю ее условия» и нажатия кнопки «Подписать».
</p>
<p>
Особый порядок принятия условий Оферты путем проставления специальной отметки (галочки) определяется интерфейсом Личного кабинета Принципала. Принципал не может зарегистрироваться в Личном кабинете и получить к нему доступ без подтверждения принятия условий Оферты.
</p>
<p>
Принимая Оферту, Принципал подтверждает, что прочел и полностью согласен с документами, размещенными на сайте в разделе, предназначенном для Принципала, которые являются неотъемлемой частью настоящей Оферты (Договора) и обязательны для исполнения Сторонами.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Общие положения</h3>
<p>
Публикуемые на сайте Агента документы (формы, требования, правила и т.п.), устанавливающие порядок и условия выполнения действий, предусмотренных настоящим Договором, являются неотъемлемой частью настоящего Договора и обязательны для исполнения Сторонами. Принципал обязан использовать формы документов, утвержденных Агентом, и не вправе вносить в них какие-либо изменения или дополнения.
</p>
<p>
Агент обязуется уведомлять Принципала обо всех изменениях в документах, связанных с исполнением настоящего Договора, путем направления электронных сообщений (через Личный кабинет или на электронную почту Принципала) или размещением уведомлений об изменениях на сайте Агентов в разделе, предназначенном для размещения объявлений.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Предмет договора</h3>
<p>
По настоящему Договору Принципал поручает, а Агент принимает на себя обязательство совершать от имени и за счет Принципала указанные действия, а Принципал обязуется выплатить Агенту вознаграждение за совершенные действия.
</p>
<p>
По настоящему Договору Агент совершает следующие действия:
</p>
<ul>
<li>Консультирование Принципала об услугах Агента, включая, помимо прочего, порядок активации и оказания услуг, работу в Личном кабинете Принципала и иные дополнительные услуги, оказываемые Агентом;</li>
<li>Совершение сделок и иных юридических действий Агентом от своего имени, но за счёт Принципала.</li>
</ul>
<p>
Настоящий Договор действует на территории Российской Федерации и иного иностранного государства.
</p>
<p>
Права и обязанности по сделкам, совершенным Агентом во исполнение настоящего Договора, возникают непосредственно у Принципала.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Права и обязанности сторон</h3>
<p>
Агент обязуется совершать действия, составляющие предмет настоящего Договора, в соответствии с законными интересами Принципала, сообщать Принципалу по его требованию все сведения о ходе исполнения настоящего Договора, передавать Принципалу в течение 7 рабочих дней имущество, полученное по сделкам.
</p>
<p>
Агент несет ответственность за сохранность документов и персональных данных, переданных ему Принципалом для исполнения настоящего Договора.
</p>
<p>
Принципал обязан без промедления принять отчет Агента, все предоставленные им документы, обеспечить Агента документами и материалами, необходимыми для выполнения настоящего Договора, возместить Агенту понесенные расходы и выплатить обусловленное Договором агентское вознаграждение.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Агентское вознаграждение и порядок оплаты</h3>
<p>
Сумма вознаграждения Агента по настоящему Договору составляет:
</p>
<ul>
<li>8% от 5 000 до 30 000 рублей</li>
<li>6% от 30 000 до 100 000 рублей</li>
<li>4% от 100 000 до 600 000 рублей</li>
</ul>
<p>
Вознаграждение выплачивается Агенту с момента подписания настоящего Договора об исполнении поручения Агентом от своего имени, но за счет Принципала.
</p>
<p>
Принципал возмещает следующие расходы Агента в сумме не более 30 000 рублей на оплату банковских услуг и иных комиссий.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Ответственность сторон</h3>
<p>
В случае нарушения Агентом сроков, установленных Договором для передачи Принципалу полученного имущества, Принципал вправе предъявить требование об уплате неустойки в размере 0,1% от непереданной суммы за каждый день просрочки.
</p>
<p>
В случае нарушения Принципалом сроков уплаты вознаграждения или возмещения расходов, Агент вправе предъявить требование об уплате неустойки в размере 0,1% от не уплаченной в срок суммы за каждый день просрочки.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Форс-мажор</h3>
<p>
Стороны освобождаются от ответственности за частичное или полное неисполнение обязательств по настоящему Договору, если это неисполнение явилось следствием возникших после заключения настоящего Договора обстоятельств непреодолимой силы.
</p>
<p>
При наступлении форс-мажорных обстоятельств каждая Сторона должна без промедления известить о них в письменном виде другую Сторону с указанием характера обстоятельств и их влияния на исполнение обязательств.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Конфиденциальность</h3>
<p>
Стороны принимают все необходимые меры для того, чтобы их сотрудники, агенты, правопреемники без предварительного согласия другой Стороны не информировали третьих лиц о конфиденциальной информации и персональных данных Сторон настоящего Договора.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Изменение и прекращение договора</h3>
<p>
Настоящий договор вступает в силу с момента его подписания и действует до момента исполнения сторонами своих обязательств по настоящему договору.
</p>
<p>
Настоящий Договор может быть изменен или прекращен по письменному соглашению Сторон, а также в других случаях, предусмотренных законодательством Российской Федерации.
</p>
<p>
Принципал вправе в любое время отказаться от исполнения настоящего Договора путем направления письменного уведомления Агенту за 3 рабочих дня.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>10. Заключительные положения</h3>
<p>
Ни одна из сторон не вправе передавать свои права и обязанности по настоящему договору третьим лицам без согласия другой стороны.
</p>
<p>
Сообщения Стороны могут направлять по факсу, электронной почте или другим способом связи при условии, что он позволяет достоверно установить, от кого исходило сообщение и кому оно адресовано.
</p>
<p>
Споры, вытекающие из настоящего Договора, разрешаются в досудебном порядке. При неурегулировании возникших разногласий спор разрешается в Арбитражном суде г. СанктПетербурга и Ленинградской области с обязательным соблюдением претензионного порядка.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Реквизиты сторон</h3>
<div className={styles.requisites}>
<p>Общество с ограниченной ответственностью «БИТФОРС»</p>
<p>196246, г. Санкт-Петербург, Московский р-н, Московское шоссе, д.25к1 литера в, помещ. 3-Н</p>
<p>ИНН / КПП: 9810001062 / 781001001</p>
<p>ОГРН: 1257800060990</p>
<p>ОКПО / ОКАТО / ОКТМО: 68342261 / 40284000000 / 40377000000</p>
<p>Руководитель: Кленин Михаил Васильевич</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Наименование банка: ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"</p>
<p>Корреспондентский счет: 30101810600000000786</p>
<p>БИК: 044030786</p>
<p>Расчетный счет: 40702810632250004861</p>
</div>
</section>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1 @@
export { ReestryPage } from './ui/ReestryPage'

View File

@@ -0,0 +1,115 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.description {
font-size: 16px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
margin-bottom: 20px;
}
.info {
font-size: 16px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
margin: 15px 0;
}
.linkBlock {
text-align: center;
padding: 30px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 8px;
margin: 30px 0;
}
.button {
display: inline-block;
padding: 15px 40px;
background: var(--interactive, #4a6dff);
color: #fff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
border: 2px solid var(--interactive, #4a6dff);
}
.button:hover {
background: transparent;
color: var(--interactive, #4a6dff);
}
.operatorInfo {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.8;
}
.operatorInfo p {
margin: 10px 0;
color: var(--text-primary, #ffffff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.linkBlock {
padding: 20px;
}
.button {
padding: 12px 30px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,74 @@
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './ReestryPage.module.css'
export function ReestryPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>Реестр операторов персональных данных</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<p className={styles.description}>
Информация об операторе персональных данных размещена в реестре операторов персональных данных Федеральной службы по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор).
</p>
<p className={styles.info}>
Вы можете просмотреть информацию об операторе в реестре Роскомнадзора, перейдя по ссылке ниже:
</p>
<div className={styles.linkBlock}>
<a
href="https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn="
target="_blank"
rel="noopener noreferrer"
className={styles.button}
>
Открыть реестр Роскомнадзора
</a>
</div>
<p className={styles.info}>
Реестр содержит информацию об операторах персональных данных, включая сведения о целях и методах обработки персональных данных, а также меры по обеспечению безопасности персональных данных.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Информация об операторе</h3>
<div className={styles.operatorInfo}>
<p>
<strong>Наименование:</strong> ООО «БИТФОРС»
</p>
<p>
<strong>ИНН:</strong> 9810001062
</p>
<p>
<strong>ОГРН:</strong> 1257800060990
</p>
<p>
<strong>Юридический адрес:</strong> 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н
</p>
<p>
<strong>Контактная информация:</strong> company@bitforcefoundation.ru
</p>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>О Роскомнадзоре</h3>
<p>
Федеральная служба по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор) это федеральный орган исполнительной власти, осуществляющий функции по контролю и надзору в области персональных данных.
</p>
<p>
Роскомнадзор ведет реестр операторов персональных данных в соответствии с требованиями Федерального закона «О персональных данных». Реестр является открытой информационной системой и доступен всем заинтересованным лицам.
</p>
</section>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1 @@
export { RegisterTestPage } from './ui/RegisterTestPage'

View File

@@ -0,0 +1,227 @@
.page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 0;
}
.card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 32px;
width: 100%;
max-width: 600px;
}
.logo {
display: flex;
justify-content: center;
margin-bottom: 28px;
}
.logo img {
height: 40px;
}
.title {
text-align: center;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 24px;
line-height: 1.3;
}
/* Переключатель типа регистрации */
.typeSwitch {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 4px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 14px;
margin-bottom: 24px;
}
.typeOption {
appearance: none;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: 600;
padding: 12px 16px;
border-radius: 10px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.typeOption:hover {
color: var(--text-primary);
}
.typeOptionActive {
background: var(--interactive);
color: #fff;
}
.twoCol {
display: grid;
grid-template-columns: 1fr;
gap: 20px 24px;
align-items: start;
}
.leftCol {
display: flex;
flex-direction: column;
gap: 20px;
}
.rightCol {
display: flex;
flex-direction: column;
gap: 8px;
}
.codeHint {
font-size: 12px;
color: var(--text-secondary);
text-decoration: underline;
cursor: pointer;
}
/* Кнопка возврата на шаг ввода данных */
.backButton {
appearance: none;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
padding: 0;
margin-bottom: 16px;
cursor: pointer;
transition: color 0.18s ease;
}
.backButton:hover {
color: var(--text-primary);
}
/* Документы для юридического лица */
.documents {
margin-top: 24px;
padding: 20px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--glass-border);
border-radius: 16px;
}
.documentsTitle {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.documentsSubtitle {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.documentsList {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.documentItem {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--glass-border);
border-radius: 12px;
}
.documentName {
font-size: 13px;
color: var(--text-primary);
line-height: 1.4;
}
.attachButton {
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: var(--interactive);
padding: 8px 14px;
border: 1px solid var(--interactive);
border-radius: 10px;
cursor: pointer;
white-space: nowrap;
transition: background 0.18s ease, color 0.18s ease;
}
.attachButton:hover {
background: var(--interactive);
color: #fff;
}
.fileInput {
display: none;
}
.error {
color: #ff5a5a;
font-size: 13px;
margin-top: 12px;
text-align: center;
}
.submitWrapper {
margin-top: 28px;
}
.legal {
text-align: center;
font-size: 11px;
color: var(--text-secondary);
margin-top: 20px;
line-height: 1.6;
}
.legal a {
color: var(--interactive);
text-decoration: none;
}
.legal a:hover {
text-decoration: underline;
}
@media (max-width: 560px) {
.card {
padding: 32px 20px;
border-radius: 0;
}
.twoCol {
grid-template-columns: 1fr;
}
.documentItem {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -0,0 +1,155 @@
import { useState } from 'react'
import { FormField, PrimaryButton, Button } from '@shared/ui'
import logo from '@shared/assets/logo-full-white.png'
import styles from './RegisterTestPage.module.css'
type AccountType = 'individual' | 'legal'
type Step = 'info' | 'documents'
const LEGAL_DOCUMENTS = [
'Свидетельство о государственной регистрации (ОГРН)',
'Свидетельство о постановке на учёт в налоговом органе (ИНН)',
'Устав организации (действующая редакция)',
'Решение/протокол о назначении руководителя',
'Документ, подтверждающий полномочия лица, открывающего счёт',
'Карточка с образцами подписей и оттиска печати',
]
export function RegisterTestPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [verificationCode, setVerificationCode] = useState('')
const [accountType, setAccountType] = useState<AccountType>('individual')
const [step, setStep] = useState<Step>('info')
const isLegal = accountType === 'legal'
// Тестовая страница — без проверок и запросов. «Создать» просто ведёт на шаг 2.
const handleInfoSubmit = (e: React.FormEvent) => {
e.preventDefault()
setStep('documents')
}
const handleDocumentsSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Тестовая страница — отправки нет.
}
return (
<div className={styles.page}>
{step === 'info' ? (
<form className={styles.card} onSubmit={handleInfoSubmit}>
<div className={styles.logo}>
<img src={logo} alt="ЭКСА" />
</div>
<h1 className={styles.title}>Создать кошелёк ЭКСА</h1>
{/* Выбор типа регистрации — перед всеми полями */}
<div className={styles.typeSwitch} role="tablist" aria-label="Тип регистрации">
<button
type="button"
role="tab"
aria-selected={!isLegal}
className={`${styles.typeOption} ${!isLegal ? styles.typeOptionActive : ''}`}
onClick={() => setAccountType('individual')}
>
Физическое лицо
</button>
<button
type="button"
role="tab"
aria-selected={isLegal}
className={`${styles.typeOption} ${isLegal ? styles.typeOptionActive : ''}`}
onClick={() => setAccountType('legal')}
>
Юридическое лицо
</button>
</div>
<div className={styles.twoCol}>
<div className={styles.leftCol}>
<FormField
label={isLegal ? 'Введите корпоративный email' : 'Введите адрес электронной почты'}
type="email"
value={email}
onChange={setEmail}
placeholder={isLegal ? 'name@company.ru' : 'example@mail.ru'}
/>
<FormField
label="Придумайте пароль"
type="password"
value={password}
onChange={setPassword}
placeholder="••••••••"
/>
<FormField
label="Повторите пароль"
type="password"
value={confirmPassword}
onChange={setConfirmPassword}
placeholder="••••••••"
/>
</div>
<div className={styles.rightCol}>
<Button variant="ghost" type="button">
Получить проверочный код
</Button>
<span className={styles.codeHint}>Код не пришёл</span>
<FormField
label="Ввести код"
type="text"
value={verificationCode}
onChange={setVerificationCode}
placeholder="000 000"
/>
</div>
</div>
<div className={styles.submitWrapper}>
<PrimaryButton label="Создать" />
</div>
<p className={styles.legal}>
Нажимая «Создать», вы принимаете<br />
<a href="#">Пользовательское соглашение</a> и <a href="#">Политику конфиденциальности</a>
</p>
</form>
) : (
/* Шаг 2: прикрепление документов (только юр. лицо) */
<form className={styles.card} onSubmit={handleDocumentsSubmit}>
<div className={styles.logo}>
<img src={logo} alt="ЭКСА" />
</div>
<button type="button" className={styles.backButton} onClick={() => setStep('info')}>
Назад к данным
</button>
<h1 className={styles.title}>Прикрепите документы</h1>
<p className={styles.documentsSubtitle}>
Для открытия счёта юридическому лицу прикрепите сканы или фотографии
следующих документов:
</p>
<ul className={styles.documentsList}>
{LEGAL_DOCUMENTS.map((doc) => (
<li key={doc} className={styles.documentItem}>
<span className={styles.documentName}>{doc}</span>
<label className={styles.attachButton}>
Прикрепить
<input type="file" className={styles.fileInput} multiple />
</label>
</li>
))}
</ul>
<div className={styles.submitWrapper}>
<PrimaryButton label="Создать аккаунт" />
</div>
</form>
)}
</div>
)
}

View File

@@ -0,0 +1 @@
export { RestorePasswordPage } from './ui/RestorePasswordPage'

View File

@@ -0,0 +1,7 @@
.page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 0;
}

View File

@@ -0,0 +1,10 @@
import { RestorePasswordForm } from '@widgets/restore-password-form'
import styles from './RestorePasswordPage.module.css'
export function RestorePasswordPage() {
return (
<div className={styles.page}>
<RestorePasswordForm />
</div>
)
}

View File

@@ -1,16 +1,18 @@
import { WalletHeader } from '@widgets/wallet-header'
import { SeedPhraseWidget } from '@widgets/seed-phrase'
import { useRevealMnemonic } from '@features/wallet'
import styles from './SeedPhrasePage.module.css'
const MOCK_WORDS = ['egg', 'phone', 'long', 'vibe', 'potato', 'soup', 'skirt', 'black', 'phase', 'word', 'num', 'cucumber']
export function SeedPhrasePage() {
const { data: mnemonic, isLoading } = useRevealMnemonic()
const words = mnemonic ? mnemonic.split(' ') : []
return (
<div className={styles.page}>
<WalletHeader />
<main className={styles.main}>
<div className={styles.glow} />
<SeedPhraseWidget words={MOCK_WORDS} />
{!isLoading && <SeedPhraseWidget words={words} />}
</main>
</div>
)

View File

@@ -0,0 +1 @@
export { SoglasiePage } from './ui/SoglasiePage'

View File

@@ -0,0 +1,127 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.subSectionTitle {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.list strong {
color: var(--text-primary, #ffffff);
}
.info {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
margin: 15px 0;
line-height: 1.8;
}
.info p {
margin: 8px 0;
color: var(--text-primary, #ffffff);
}
.contacts {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.8;
}
.contacts p {
margin: 8px 0;
color: var(--text-primary, #ffffff);
}
.confirmation {
padding: 10px 0;
color: var(--text-primary, #ffffff);
font-weight: 500;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.sectionTitle {
font-size: 16px;
}
.subSectionTitle {
font-size: 14px;
}
.list {
margin-left: 15px;
}
.info,
.contacts {
padding: 15px;
}
}

View File

@@ -0,0 +1,300 @@
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './SoglasiePage.module.css'
export function SoglasiePage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>СОГЛАСИЕ НА ОБРАБОТКУ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Преамбула</h3>
<p>
Я, субъект персональных данных, действуя своей волей и в своем интересе, в соответствии с требованиями Федерального закона от 27.07.2006 152-ФЗ «О персональных данных», предоставляю ООО «БИТФОРС» согласие на обработку моих персональных данных на условиях и для целей, определенных настоящим Согласием.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Сведения об операторе</h3>
<div className={styles.info}>
<p>Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Веб-сайт: https://bitforce-foundation.ru</p>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Правовые основания обработки</h3>
<p>
Настоящее согласие предоставляется на основании пункта 1 части 1 статьи 6 Федерального закона «О персональных данных» и является правовым основанием для обработки персональных данных Оператором.
</p>
<p>Согласие дается добровольно, своей волей и в своих интересах.</p>
<p>
Субъект персональных данных понимает последствия предоставления согласия, включая возможные риски, связанные с обработкой персональных данных.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Цели обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>3.1. Основные цели:</h4>
<ul className={styles.list}>
<li>Регистрация и ведение учетной записи на веб-сайте и в мобильном приложении</li>
<li>Идентификация и верификация личности в соответствии с требованиями законодательства</li>
<li>Предоставление услуг по обмену криптовалют и электронных денежных средств</li>
<li>Проведение финансовых операций, переводов и расчетов</li>
<li>Ведение учета и истории операций</li>
</ul>
<h4 className={styles.subSectionTitle}>3.2. Дополнительные цели:</h4>
<ul className={styles.list}>
<li>Обеспечение безопасности операций и предотвращение мошенничества</li>
<li>Выполнение требований по противодействию легализации доходов</li>
<li>Соблюдение требований валютного, налогового и иного применимого законодательства</li>
<li>Предоставление технической поддержки и клиентского сервиса</li>
<li>Рассылка уведомлений о состоянии операций и изменениях в условиях</li>
</ul>
<h4 className={styles.subSectionTitle}>3.3. Маркетинговые цели (при дополнительном согласии):</h4>
<ul className={styles.list}>
<li>Направление информационных и рекламных материалов</li>
<li>Проведение маркетинговых исследований и опросов</li>
<li>Персонализация предложений и услуг</li>
<li>Анализ предпочтений и поведения для улучшения сервисов</li>
</ul>
<h4 className={styles.subSectionTitle}>3.4. Аналитические цели:</h4>
<ul className={styles.list}>
<li>Анализ использования веб-сайта и мобильного приложения</li>
<li>Улучшение качества предоставляемых услуг</li>
<li>Разработка новых продуктов и сервисов</li>
<li>Создание статистических отчетов в обезличенном виде</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Перечень персональных данных</h3>
<h4 className={styles.subSectionTitle}>4.1. Идентификационные данные:</h4>
<ul className={styles.list}>
<li>Фамилия, имя, отчество</li>
<li>Дата рождения</li>
<li>Гражданство</li>
</ul>
<h4 className={styles.subSectionTitle}>4.2. Документы, удостоверяющие личность:</h4>
<ul className={styles.list}>
<li>Серия и номер паспорта гражданина Российской Федерации</li>
<li>Дата выдачи и код подразделения</li>
<li>Адрес регистрации по месту жительства</li>
<li>Цифровые копии (сканы) документов</li>
</ul>
<h4 className={styles.subSectionTitle}>4.3. Контактная информация:</h4>
<ul className={styles.list}>
<li>Номера телефонов (мобильный, домашний, рабочий)</li>
<li>Адреса электронной почты</li>
<li>Почтовые адреса (фактического проживания, для корреспонденции)</li>
</ul>
<h4 className={styles.subSectionTitle}>4.4. Финансовая информация:</h4>
<ul className={styles.list}>
<li>Номера банковских счетов и реквизиты банковских карт</li>
<li>Реквизиты криптовалютных кошельков и адресов</li>
<li>Информация о доходах и источниках происхождения денежных средств</li>
<li>История финансовых операций и транзакций</li>
</ul>
<h4 className={styles.subSectionTitle}>4.5. Техническая информация:</h4>
<ul className={styles.list}>
<li>IP-адреса устройств, с которых осуществляется доступ к сервисам</li>
<li>Информация о браузере, операционной системе и устройстве</li>
<li>Файлы cookie и данные локального хранилища</li>
<li>Логи действий и история использования сервисов</li>
</ul>
<h4 className={styles.subSectionTitle}>4.6. Дополнительная информация:</h4>
<ul className={styles.list}>
<li>Фотографии для процедур верификации</li>
<li>Видеозаписи процедур видеоидентификации</li>
<li>Биометрические данные (при использовании соответствующих технологий)</li>
<li>Информация о семейном положении и профессиональной деятельности</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Перечень действий с персональными данными</h3>
<p>Согласие распространяется на следующие действия (операции) с персональными данными:</p>
<ul className={styles.list}>
<li>Сбор, запись и первичная обработка персональных данных</li>
<li>Накопление и систематизация в базах данных</li>
<li>Создание резервных копий и архивирование</li>
<li>Извлечение, использование и анализ данных</li>
<li>Уточнение, обновление и актуализация информации</li>
<li>Передача данных третьим лицам</li>
<li>Обезличивание и удаление данных</li>
<li>Автоматизированная обработка и профилирование</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Лица, которым могут быть переданы персональные данные</h3>
<h4 className={styles.subSectionTitle}>6.1. Сотрудники Оператора:</h4>
<ul className={styles.list}>
<li>Уполномоченные сотрудники, непосредственно участвующие в обработке</li>
<li>Сотрудники службы безопасности и комплаенса</li>
<li>Сотрудники технической поддержки</li>
<li>Руководящий состав в рамках их полномочий</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2. Государственные и муниципальные органы:</h4>
<ul className={styles.list}>
<li>Федеральная служба по финансовому мониторингу</li>
<li>Федеральная налоговая служба</li>
<li>Правоохранительные органы (при наличии законных требований)</li>
<li>Суды и органы исполнения судебных решений</li>
</ul>
<h4 className={styles.subSectionTitle}>6.3. Партнеры и контрагенты:</h4>
<ul className={styles.list}>
<li>Банки и платежные системы</li>
<li>Операторы электронных денежных средств</li>
<li>Поставщики технологических решений</li>
<li>Аудиторские и консалтинговые организации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Сроки обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>7.1. Общие принципы:</h4>
<p>
Персональные данные обрабатываются в течение времени, необходимого для достижения целей обработки. После достижения целей данные подлежат уничтожению или обезличиванию.
</p>
<h4 className={styles.subSectionTitle}>7.2. Конкретные сроки обработки:</h4>
<ul className={styles.list}>
<li>
<strong>Данные активных клиентов:</strong> в течение всего периода отношений плюс 5 лет после прекращения
</li>
<li>
<strong>Данные для идентификации:</strong> 5 лет с момента прекращения отношений
</li>
<li>
<strong>Финансовая информация:</strong> 5 лет с даты совершения операции
</li>
<li>
<strong>Маркетинговые данные:</strong> до отзыва согласия, но не более 3 лет
</li>
<li>
<strong>Техническая информация:</strong> 1 год для безопасности, 6 месяцев для логов
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Права субъекта персональных данных</h3>
<h4 className={styles.subSectionTitle}>8.1. Право на информацию:</h4>
<ul className={styles.list}>
<li>Получение подтверждения факта обработки персональных данных</li>
<li>Получение информации о целях и способах обработки</li>
<li>Информация о сроках обработки и составе данных</li>
<li>Сведения о лицах, которым передаются данные</li>
</ul>
<h4 className={styles.subSectionTitle}>8.2. Право на доступ:</h4>
<ul className={styles.list}>
<li>Получение копий обрабатываемых персональных данных</li>
<li>Ознакомление с историей обработки и изменений</li>
<li>Получение информации об источниках персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>8.3. Право на исправление и удаление:</h4>
<ul className={styles.list}>
<li>Требование исправления неточных или неполных данных</li>
<li>Требование удаления персональных данных при наличии оснований</li>
<li>Удаление данных после отзыва согласия</li>
</ul>
<h4 className={styles.subSectionTitle}>8.4. Право на отзыв согласия:</h4>
<ul className={styles.list}>
<li>Согласие может быть отозвано в любое время</li>
<li>Отзыв оформляется в письменной форме</li>
<li>После отзыва обработка прекращается в разумные сроки</li>
</ul>
<h4 className={styles.subSectionTitle}>8.5. Право на обжалование:</h4>
<ul className={styles.list}>
<li>Обращение к Оператору с жалобами на действия по обработке данных</li>
<li>Обращение в Роскомнадзор или его территориальные органы</li>
<li>Обращение в суд для защиты нарушенных прав</li>
</ul>
<h4 className={styles.subSectionTitle}>8.6. Порядок реализации прав:</h4>
<ul className={styles.list}>
<li>Обращения направляются на адрес: company@bitforcefoundation.ru</li>
<li>Обращения рассматриваются в течение 30 дней</li>
<li>При необходимости срок может быть продлен на 30 дней</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Заключительные положения</h3>
<h4 className={styles.subSectionTitle}>9.1. Действие согласия:</h4>
<ul className={styles.list}>
<li>Согласие действует с момента его предоставления</li>
<li>Согласие действует до его отзыва или до достижения целей обработки</li>
<li>При существенных изменениях целей требуется новое согласие</li>
</ul>
<h4 className={styles.subSectionTitle}>9.2. Форма предоставления согласия:</h4>
<ul className={styles.list}>
<li>Согласие может быть предоставлено в письменной форме</li>
<li>Согласие может быть предоставлено в электронной форме</li>
<li>Согласие может выражаться путем совершения конклюдентных действий</li>
</ul>
<h4 className={styles.subSectionTitle}>9.3. Последствия непредоставления согласия:</h4>
<ul className={styles.list}>
<li>Отказ в предоставлении согласия может повлечь невозможность регистрации</li>
<li>Отказ может ограничить доступ к отдельным услугам</li>
<li>Отказ в согласии на маркетинг не влияет на основные услуги</li>
<li>Субъект вправе предоставить частичное согласие</li>
</ul>
<h4 className={styles.subSectionTitle}>9.4. Контактная информация:</h4>
<div className={styles.contacts}>
<p>Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Ответственное лицо: Кленин Михаил Васильевич</p>
<p>Официальный сайт: https://bitforce-foundation.ru</p>
</div>
<h4 className={styles.subSectionTitle}>9.5. Подтверждение понимания:</h4>
<p className={styles.confirmation}>
Предоставляя настоящее согласие, я подтверждаю, что:
</p>
<ul className={styles.list}>
<li>Ознакомлен с содержанием согласия и понимаю его значение</li>
<li>Понимаю цели и способы обработки моих персональных данных</li>
<li>Знаю о своих правах и способах их реализации</li>
<li>Согласие предоставляется добровольно и осознанно</li>
<li>Имею возможность отозвать согласие в любое время</li>
</ul>
</section>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -1,44 +1,4 @@
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--bg-deep);
}
.tabs {
display: flex;
gap: 8px;
padding: 24px 28px 0;
}
.tab {
padding: 10px 24px;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
border: none;
font-family: var(--font-sans);
letter-spacing: 0.5px;
transition: all 0.2s;
}
.active {
background: linear-gradient(135deg, var(--grad-edge), var(--grad-center));
color: var(--text-primary);
}
.inactive {
background: rgba(255, 255, 255, 0.06);
color: var(--text-secondary);
}
.inactive:hover {
color: var(--text-primary);
}
.main {
flex: 1;
.content {
display: flex;
flex-direction: column;
align-items: center;
@@ -46,7 +6,7 @@
}
@media (max-width: 650px) {
.main {
.content {
padding: 32px 20px;
}
}

View File

@@ -1,38 +1,14 @@
import { useState } from 'react'
import { Footer } from '@widgets/footer'
import { SwapForm } from '@widgets/swap-form'
import { WalletHeader } from '@widgets/wallet-header'
import { SwapBridgeTabs } from '@widgets/swap-bridge-tabs'
import styles from './SwapPage.module.css'
type Tab = 'swap' | 'bridge'
export function SwapPage() {
const [tab, setTab] = useState<Tab>('swap')
return (
<div className={styles.page}>
<WalletHeader />
<div className={styles.tabs}>
<button
className={`${styles.tab} ${tab === 'swap' ? styles.active : styles.inactive}`}
onClick={() => setTab('swap')}
>
СВОП
</button>
<button
className={`${styles.tab} ${tab === 'bridge' ? styles.active : styles.inactive}`}
onClick={() => setTab('bridge')}
>
БРИДЖ
</button>
</div>
<main className={styles.main}>
<>
<SwapBridgeTabs active="swap" />
<div className={styles.content}>
<SwapForm />
</main>
<Footer />
</div>
</>
)
}

View File

@@ -0,0 +1 @@
export { TransactionsPage } from './ui/TransactionsPage'

View File

@@ -0,0 +1,39 @@
.inner {
padding: 28px 32px 40px;
max-width: 1200px;
width: 100%;
margin: 0 auto;
position: relative;
}
.glow {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 320px;
background: radial-gradient(ellipse, rgba(61, 42, 142, 0.15), transparent 70%);
pointer-events: none;
z-index: 0;
}
.title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 24px;
position: relative;
z-index: 1;
}
@media (max-width: 900px) {
.inner {
padding: 20px 16px 32px;
}
.glow {
width: auto;
height: auto;
}
}

View File

@@ -0,0 +1,12 @@
import { TransactionsList } from '@widgets/transactions-list'
import styles from './TransactionsPage.module.css'
export function TransactionsPage() {
return (
<div className={styles.inner}>
<div className={styles.glow} />
<h1 className={styles.title}>Транзакции</h1>
<TransactionsList />
</div>
)
}

View File

@@ -35,6 +35,20 @@
font-size: 14px;
}
.noWallet {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
min-height: 300px;
color: var(--text-primary);
font-size: 16px;
text-align: center;
position: relative;
z-index: 1;
}
@media (max-width: 992px) {
.glow {
width: auto;

View File

@@ -1,25 +1,53 @@
import { Navigate } from 'react-router-dom'
import { Navigate, useNavigate, useParams } from 'react-router-dom'
import { useMe } from '@features/auth'
import { ROUTES } from '@shared/config/routes'
import { usePortfolio, useCreateWallet, CHAINS, type Chain } from '@features/wallet'
import { BalanceCard } from '@widgets/balance-card'
import { TokenTable } from '@widgets/token-table'
import { TokenTable, AllTokenTable } from '@widgets/token-table'
import { WalletHeader } from '@widgets/wallet-header'
import { WalletChainTabs } from '@widgets/wallet-chain-tabs'
import { Button } from '@shared/ui'
import styles from './WalletPage.module.css'
export function WalletPage() {
const { data, isLoading, isError } = useMe()
const { error: portfolioError } = usePortfolio()
const { mutate: createWallet, isPending } = useCreateWallet()
const navigate = useNavigate()
const { chain: chainParam } = useParams<{ chain?: string }>()
const noWallet = (portfolioError as { error?: string } | null)?.error?.includes('No wallets')
if (isLoading) return null
if (isError) return <div className={styles.error}>Произошла ошибка. Попробуйте обновить страницу.</div>
if (data && !data.kyc_verified) return <Navigate to={ROUTES.KYC} replace />
const upper = chainParam?.toUpperCase() as Chain | undefined
const chain: Chain | undefined = upper && CHAINS.includes(upper) ? upper : undefined
return (
<div className={styles.page}>
<WalletHeader />
<main className={styles.main}>
<div className={styles.glow} />
{noWallet ? (
<div className={styles.noWallet}>
<p>У вас пока нет кошелька. Создайте его, чтобы начать.</p>
<Button
variant="outline"
onClick={() => createWallet(undefined, { onSuccess: () => navigate(ROUTES.SEED_PHRASE) })}
disabled={isPending}
>
{isPending ? 'Создание...' : 'Создать кошелёк'}
</Button>
</div>
) : (
<>
<BalanceCard />
<TokenTable />
<WalletChainTabs />
{chain ? <TokenTable chain={chain} /> : <AllTokenTable />}
</>
)}
</main>
</div>
)

View File

@@ -6,17 +6,28 @@ interface CsrfResponse {
}
let cachedToken: string | null = null
let inflight: Promise<string> | null = null
export function clearCsrfCache(): void {
cachedToken = null
inflight = null
}
export async function getCsrfToken(): Promise<string> {
if (cachedToken) return cachedToken
const res = await fetch(`${API_URL}/csrf/token`, {
credentials: 'include',
})
const data: CsrfResponse = await res.json()
export function getCsrfToken(): Promise<string> {
if (cachedToken) return Promise.resolve(cachedToken)
if (inflight) return inflight
inflight = fetch(`${API_URL}/csrf/token`, { credentials: 'include' })
.then((res) => res.json() as Promise<CsrfResponse>)
.then((data) => {
cachedToken = data.token
inflight = null
return cachedToken
})
.catch((err) => {
inflight = null
throw err
})
return inflight
}

Some files were not shown because too many files have changed in this diff Show More