diff --git a/.github/ISSUE_TEMPLATE/bug-bedolage.md b/.github/ISSUE_TEMPLATE/bug-bedolage.md
index fd68c55..67474be 100644
--- a/.github/ISSUE_TEMPLATE/bug-bedolage.md
+++ b/.github/ISSUE_TEMPLATE/bug-bedolage.md
@@ -1,10 +1,9 @@
---
name: Bug Bedolage
about: Создать баг-репорт «Бедолаге»
-title: "[BUG] Заголовок"
+title: '[BUG] Заголовок'
labels: ''
assignees: ''
-
---
## Описание / Конфигурация
@@ -19,15 +18,15 @@ assignees: ''
## Шаги для воспроизведения
-1.
-2.
-3.
+1.
+2.
+3.
### Фактический результат
-*
-*
-*
+-
+-
+-
### Скрины поведения
diff --git a/.github/ISSUE_TEMPLATE/feat-bedolage.md b/.github/ISSUE_TEMPLATE/feat-bedolage.md
index 37cc450..fe6c6e2 100644
--- a/.github/ISSUE_TEMPLATE/feat-bedolage.md
+++ b/.github/ISSUE_TEMPLATE/feat-bedolage.md
@@ -1,10 +1,9 @@
---
name: Feat Bedolage
about: Создать запрос на фичу «Бедолаге»
-title: "[FEAT] Заголовок"
+title: '[FEAT] Заголовок'
labels: ''
assignees: ''
-
---
## Описание / Конфигурация
diff --git a/CHANGELOG.md b/CHANGELOG.md
index def85bb..702007f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,102 +2,96 @@
## [1.7.0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.6.2...v1.7.0) (2026-02-04)
-
### Features
-* add Stars payment confirmation and admin validation ([e6f8ae6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e6f8ae6ab09c431d5322c851165f30469678ed72))
-* replace payment modals with page-based navigation ([576893f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/576893f5c6b67c19bee0cd562cd0430a88350619))
-
+- add Stars payment confirmation and admin validation ([e6f8ae6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e6f8ae6ab09c431d5322c851165f30469678ed72))
+- replace payment modals with page-based navigation ([576893f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/576893f5c6b67c19bee0cd562cd0430a88350619))
### Bug Fixes
-* dim accent color for background blobs ([bb32cd8](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bb32cd8757b116728c0e7357fc40bcb842e7a476))
-* inline Stars confirmation and unified payment type display ([8068f84](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8068f847247307aa3adae1f6965987882be8a785))
-* prevent payment type reset after wheel spin ([4499c9a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4499c9ad57dcdcd3126c8d4261bc9f32accd21d7))
-* unify wheel Stars payment across desktop and mobile ([02640d1](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/02640d1c38dde00d431058a399adcc85fd9bcaac))
-* update Aurora colors reactively without recreating WebGL context ([59a251c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/59a251cb8c706b3029990e58fc6003ce620f80d3))
+- dim accent color for background blobs ([bb32cd8](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bb32cd8757b116728c0e7357fc40bcb842e7a476))
+- inline Stars confirmation and unified payment type display ([8068f84](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8068f847247307aa3adae1f6965987882be8a785))
+- prevent payment type reset after wheel spin ([4499c9a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4499c9ad57dcdcd3126c8d4261bc9f32accd21d7))
+- unify wheel Stars payment across desktop and mobile ([02640d1](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/02640d1c38dde00d431058a399adcc85fd9bcaac))
+- update Aurora colors reactively without recreating WebGL context ([59a251c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/59a251cb8c706b3029990e58fc6003ce620f80d3))
## [1.6.2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.6.1...v1.6.2) (2026-02-04)
-
### Bug Fixes
-* add theme toggle to desktop header and sync theme across components ([bf00d37](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bf00d37b4af799130e3dd8cd2c083ec933833281))
-* Aurora animation ignoring light theme background color ([c1dc019](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c1dc019c8b2819e13da42ad1c2648740a09279d1))
-* replace individual light theme overrides with CSS variable swap ([9ac00c9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/9ac00c94a6805577d6ac71e83fae5032217a31c9))
-* use dynamic champagne variables for light theme palette swap ([ecd912b](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ecd912b16a63b692b45361cfa53e7cacb0cb3e4f))
-* use theme surface and background colors for Aurora animation ([a91e055](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/a91e0555979540f50ca1afef5ee8c91b162c4a7f))
+- add theme toggle to desktop header and sync theme across components ([bf00d37](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bf00d37b4af799130e3dd8cd2c083ec933833281))
+- Aurora animation ignoring light theme background color ([c1dc019](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c1dc019c8b2819e13da42ad1c2648740a09279d1))
+- replace individual light theme overrides with CSS variable swap ([9ac00c9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/9ac00c94a6805577d6ac71e83fae5032217a31c9))
+- use dynamic champagne variables for light theme palette swap ([ecd912b](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ecd912b16a63b692b45361cfa53e7cacb0cb3e4f))
+- use theme surface and background colors for Aurora animation ([a91e055](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/a91e0555979540f50ca1afef5ee8c91b162c4a7f))
## [1.6.1](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.6.0...v1.6.1) (2026-02-04)
-
### Bug Fixes
-* add fallback recovery for Telegram popup callback not firing ([7ac7db4](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7ac7db4ddb2950216334be01db37f185594bea6e))
-* add HMR guard to prevent ConcurrentCallError on SDK double-init ([bcbda17](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bcbda17220357815ee2df269e293db0fecee7bd3))
-* get fresh Telegram WebApp reference on each popup call ([792fb1e](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/792fb1ed8a1cc4d4a5350556308e00e8cad5313a))
-* prevent duplicate Telegram popup opening ([71647eb](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/71647ebc8795fc57f958d4fdedfb2f9c0b23837e))
-* prevent popup cascade when Telegram callback doesn't fire ([2d00a5c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/2d00a5c21fff339f229d8fe2001a898d15722cdd))
-* resolve SDK v3 mount errors, back button and fullscreen not working ([61e3910](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/61e3910981e401fbc0b968615307e5101f6f96e9))
-* use direct dialog.popup call instead of useDestructiveConfirm ([ef77276](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ef77276246fdb60255f625289542584b83c93fcc))
+- add fallback recovery for Telegram popup callback not firing ([7ac7db4](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7ac7db4ddb2950216334be01db37f185594bea6e))
+- add HMR guard to prevent ConcurrentCallError on SDK double-init ([bcbda17](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bcbda17220357815ee2df269e293db0fecee7bd3))
+- get fresh Telegram WebApp reference on each popup call ([792fb1e](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/792fb1ed8a1cc4d4a5350556308e00e8cad5313a))
+- prevent duplicate Telegram popup opening ([71647eb](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/71647ebc8795fc57f958d4fdedfb2f9c0b23837e))
+- prevent popup cascade when Telegram callback doesn't fire ([2d00a5c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/2d00a5c21fff339f229d8fe2001a898d15722cdd))
+- resolve SDK v3 mount errors, back button and fullscreen not working ([61e3910](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/61e3910981e401fbc0b968615307e5101f6f96e9))
+- use direct dialog.popup call instead of useDestructiveConfirm ([ef77276](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ef77276246fdb60255f625289542584b83c93fcc))
## [1.6.0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.5.0...v1.6.0) (2026-02-04)
-
### Features
-* add animated MovingGradient background ([24781f3](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/24781f32ec0b7889ddb4a88d994d91b6d8593dec))
-* add autocomplete to settings search ([e5096d5](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e5096d571ff0b9ea3fb9c08c1cbece46a1ece656))
-* add locales for user search in promo offer sending ([0c9d092](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/0c9d09280c600e03c8cd8b7f51c8eb27c664c22c))
-* add Telegram/Email channel selection to broadcast create page ([0773afd](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/0773afdf6e37f0b8da679a176048764110795919))
-* add useNotify hook for unified notifications ([6f4d1ef](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/6f4d1ef08587a671a3060c304bc8603df5d9a17e))
-* add user search autocomplete for promo offer sending ([fc92267](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/fc922671d2fe2de7e4961205158c8e7e3404020a))
-* extract promocodes and promo groups into separate pages ([a96ddde](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/a96ddde314d938c2ee2e7c8fe7eaab84007060df))
-* improve tariff builder UI ([e19767a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e19767af82ed84c80f1e5cb0e9535962d360fd54))
-* Linear-style UI redesign with improved mobile experience ([b953ee0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/b953ee0b8c79c6340eca0467d65fee22d362875e))
-* migrate to [@tma](https://github.com/tma).js/sdk-react for Telegram Mini App ([edb5be0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/edb5be09ae372b6ee2985484518bdf76d87b89e4))
-* move campaign statistics to dedicated page ([1027deb](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/1027deb134d58f72f40b2f18878e8700683a4c86))
-* move ticket settings to dedicated page ([ead4606](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ead4606bb59e59c50445c7b7198abf42c54e1326))
-* redesign fortune wheel UI and add to mobile nav ([7e2802c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7e2802c5b5ee4bae1cf8d07009056e9c66688197))
-* redesign fortune wheel with improved UX ([494285b](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/494285bcbf99d8f74873dcf571b0a780e968100b))
-* replace broadcast creation modal with dedicated page ([175516e](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/175516ec9bc85575483fa7223838e5f52e4cfe7b))
-* replace campaign creation modal with dedicated page ([bce4d94](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bce4d94229c81d67a7621faccf9a69bd6b61d5e9))
-* replace MovingGradient with Aurora WebGL background ([cffef41](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/cffef41f634f940da07645461420f32554e6da9d))
-* replace tariff creation modal with dedicated page ([dc17695](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/dc1769520612286bc2da4bd25c23a60ab38792f3))
-* scroll to tariffs section when clicking discount badge ([3613294](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/3613294a7869e6866d3f3d5d2dccde4102e05b9f))
-
+- add animated MovingGradient background ([24781f3](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/24781f32ec0b7889ddb4a88d994d91b6d8593dec))
+- add autocomplete to settings search ([e5096d5](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e5096d571ff0b9ea3fb9c08c1cbece46a1ece656))
+- add locales for user search in promo offer sending ([0c9d092](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/0c9d09280c600e03c8cd8b7f51c8eb27c664c22c))
+- add Telegram/Email channel selection to broadcast create page ([0773afd](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/0773afdf6e37f0b8da679a176048764110795919))
+- add useNotify hook for unified notifications ([6f4d1ef](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/6f4d1ef08587a671a3060c304bc8603df5d9a17e))
+- add user search autocomplete for promo offer sending ([fc92267](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/fc922671d2fe2de7e4961205158c8e7e3404020a))
+- extract promocodes and promo groups into separate pages ([a96ddde](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/a96ddde314d938c2ee2e7c8fe7eaab84007060df))
+- improve tariff builder UI ([e19767a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e19767af82ed84c80f1e5cb0e9535962d360fd54))
+- Linear-style UI redesign with improved mobile experience ([b953ee0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/b953ee0b8c79c6340eca0467d65fee22d362875e))
+- migrate to [@tma](https://github.com/tma).js/sdk-react for Telegram Mini App ([edb5be0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/edb5be09ae372b6ee2985484518bdf76d87b89e4))
+- move campaign statistics to dedicated page ([1027deb](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/1027deb134d58f72f40b2f18878e8700683a4c86))
+- move ticket settings to dedicated page ([ead4606](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ead4606bb59e59c50445c7b7198abf42c54e1326))
+- redesign fortune wheel UI and add to mobile nav ([7e2802c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7e2802c5b5ee4bae1cf8d07009056e9c66688197))
+- redesign fortune wheel with improved UX ([494285b](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/494285bcbf99d8f74873dcf571b0a780e968100b))
+- replace broadcast creation modal with dedicated page ([175516e](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/175516ec9bc85575483fa7223838e5f52e4cfe7b))
+- replace campaign creation modal with dedicated page ([bce4d94](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/bce4d94229c81d67a7621faccf9a69bd6b61d5e9))
+- replace MovingGradient with Aurora WebGL background ([cffef41](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/cffef41f634f940da07645461420f32554e6da9d))
+- replace tariff creation modal with dedicated page ([dc17695](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/dc1769520612286bc2da4bd25c23a60ab38792f3))
+- scroll to tariffs section when clicking discount badge ([3613294](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/3613294a7869e6866d3f3d5d2dccde4102e05b9f))
### Bug Fixes
-* add full i18n support for RemnaWave section and improve sync UI ([ed86dfa](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ed86dfa8bd0c680f842bc17ef7663ee1d266ee11))
-* add missing i18n keys for broadcast detail page ([c60a242](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c60a242f1da4b03d44d56485d83313755a5a0c8e))
-* add placeholders to all tariff form number inputs ([8cd95b8](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8cd95b84fb703104d436d4ce417d3ab57aa66f9f))
-* add Russian translation for device limit reduction reason ([e884860](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e884860ab86b6cbcdb9db2858f47bd3813a18740))
-* add Telegram header padding for Android fullscreen mode ([093c9f2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/093c9f28935133cd300bf4c481d5c798011ecbfe))
-* AdminTariffCreate back button and daily tariff colors ([d623cd4](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/d623cd41e9e9e15733c68b9640f0296f37043468))
-* allow clearing number inputs and add validation ([47e28ee](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/47e28ee78fd417db86ad4a026f5983f68f0e1c76))
-* Aurora uses theme colors from API with blur overlay ([55ae55f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/55ae55f4af1050207ceac6db992f6bf66ce4b77b))
-* disable Telegram swipe-to-close globally to prevent accidental app closures ([9b0be28](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/9b0be280d292179fb6e9f219c21d18ce1ae7e5fc))
-* disable Telegram vertical swipes during drag operations ([8deca2f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8deca2fa5bf1da2ba76191616d4a1638d1d79f9d))
-* handle unmounted SDK components gracefully ([baa57b9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/baa57b907ecb3ada3623eafcffbc070a56e1915c))
-* improve admin user detail tabs scroll and sync buttons design ([45dac03](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/45dac039f9e77f55fefe6ea1e761edd037bd13d8))
-* improve header layout for mobile - stack button below title ([643d4fd](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/643d4fd3af891fc9a9e27501dae715f91242f200))
-* improve mobile layout for bandwidth and add pluralization support ([e9af285](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e9af285dadeeb0ce951940be46e07561f61844d9))
-* improve tariff delete button visibility ([5b30f24](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/5b30f24e7e3bd672464a0314433d8649267e7828))
-* improve Toast visibility and allow tariff deletion with subscriptions ([36cc01c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/36cc01ca7e2489d603d96673f93237648832223e))
-* make Aurora colors vibrant and increase speed ([851e6a3](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/851e6a353bbf5c48c7cdf36ba1d9fd821c775aff))
-* promo offer button mobile layout ([1d4a99c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/1d4a99c47432128402d213a3be3cbd9f316e6171))
-* remove dark backgrounds causing black rectangles ([2926a5a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/2926a5a89c522b2133b9f56eef950031c0f7f2c1))
-* remove page transition animations to prevent flashing ([dda8323](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/dda8323b452bd3d3054d5bbdd607ce5c7a27a6a4))
-* remove quick actions section and optimize build chunks ([de613d9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/de613d909d64b4e1853032447df671939ddad9b2))
-* remove small discount badge and improve large badge UX ([822b9a6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/822b9a6265e7c9b14c4ef630e80ee81ef9597496))
-* restore page animations, improve checkbox visibility ([5ad5e8d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/5ad5e8d3657b8074305fd1c8b94cebd5c6cb4af2))
-* revert to native Telegram WebApp API, remove SDK usage ([6f8bc4f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/6f8bc4fca592f074d4705069a48bbaef3e5a105d))
-* scroll to start of tariffs section and wait for data to load ([c815ac2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c815ac29ea281056908f39dfb6a390b3b201377b))
-* UI improvements - reduce Android header, hide mobile scrollbar, disable animations in Telegram, consistent menu overlay ([768b340](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/768b340c35c5f145940bb9966e2e6713039be32f))
-* unify card styles across the project ([4a25d8d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4a25d8df03b26f25800f882e46223bab64873b73))
-* use native Telegram popup for email preview restriction ([7aeb47f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7aeb47f583056d651b90db4ed7cad2d2a2aef3b1))
-* use pill-style tabs in admin user detail page ([09584fc](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/09584fc901d778453fbda437d643f4bd9c4321e3))
-* use RemnaWave icon in admin panel menu ([4034b4d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4034b4db3932cab273636bb54bf796aff6de712c))
-* use single shared WebSocket connection and optimize build chunks ([f6854c6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/f6854c6c3aef3ff46d7839dfe94d2ec6bf4d7d64))
-* wrap all SDK isSupported() calls in try-catch ([e5ea09d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e5ea09dd3a13ec47ca65de93c6f5858c1fadb135))
+- add full i18n support for RemnaWave section and improve sync UI ([ed86dfa](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/ed86dfa8bd0c680f842bc17ef7663ee1d266ee11))
+- add missing i18n keys for broadcast detail page ([c60a242](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c60a242f1da4b03d44d56485d83313755a5a0c8e))
+- add placeholders to all tariff form number inputs ([8cd95b8](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8cd95b84fb703104d436d4ce417d3ab57aa66f9f))
+- add Russian translation for device limit reduction reason ([e884860](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e884860ab86b6cbcdb9db2858f47bd3813a18740))
+- add Telegram header padding for Android fullscreen mode ([093c9f2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/093c9f28935133cd300bf4c481d5c798011ecbfe))
+- AdminTariffCreate back button and daily tariff colors ([d623cd4](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/d623cd41e9e9e15733c68b9640f0296f37043468))
+- allow clearing number inputs and add validation ([47e28ee](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/47e28ee78fd417db86ad4a026f5983f68f0e1c76))
+- Aurora uses theme colors from API with blur overlay ([55ae55f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/55ae55f4af1050207ceac6db992f6bf66ce4b77b))
+- disable Telegram swipe-to-close globally to prevent accidental app closures ([9b0be28](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/9b0be280d292179fb6e9f219c21d18ce1ae7e5fc))
+- disable Telegram vertical swipes during drag operations ([8deca2f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/8deca2fa5bf1da2ba76191616d4a1638d1d79f9d))
+- handle unmounted SDK components gracefully ([baa57b9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/baa57b907ecb3ada3623eafcffbc070a56e1915c))
+- improve admin user detail tabs scroll and sync buttons design ([45dac03](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/45dac039f9e77f55fefe6ea1e761edd037bd13d8))
+- improve header layout for mobile - stack button below title ([643d4fd](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/643d4fd3af891fc9a9e27501dae715f91242f200))
+- improve mobile layout for bandwidth and add pluralization support ([e9af285](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e9af285dadeeb0ce951940be46e07561f61844d9))
+- improve tariff delete button visibility ([5b30f24](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/5b30f24e7e3bd672464a0314433d8649267e7828))
+- improve Toast visibility and allow tariff deletion with subscriptions ([36cc01c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/36cc01ca7e2489d603d96673f93237648832223e))
+- make Aurora colors vibrant and increase speed ([851e6a3](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/851e6a353bbf5c48c7cdf36ba1d9fd821c775aff))
+- promo offer button mobile layout ([1d4a99c](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/1d4a99c47432128402d213a3be3cbd9f316e6171))
+- remove dark backgrounds causing black rectangles ([2926a5a](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/2926a5a89c522b2133b9f56eef950031c0f7f2c1))
+- remove page transition animations to prevent flashing ([dda8323](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/dda8323b452bd3d3054d5bbdd607ce5c7a27a6a4))
+- remove quick actions section and optimize build chunks ([de613d9](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/de613d909d64b4e1853032447df671939ddad9b2))
+- remove small discount badge and improve large badge UX ([822b9a6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/822b9a6265e7c9b14c4ef630e80ee81ef9597496))
+- restore page animations, improve checkbox visibility ([5ad5e8d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/5ad5e8d3657b8074305fd1c8b94cebd5c6cb4af2))
+- revert to native Telegram WebApp API, remove SDK usage ([6f8bc4f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/6f8bc4fca592f074d4705069a48bbaef3e5a105d))
+- scroll to start of tariffs section and wait for data to load ([c815ac2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/c815ac29ea281056908f39dfb6a390b3b201377b))
+- UI improvements - reduce Android header, hide mobile scrollbar, disable animations in Telegram, consistent menu overlay ([768b340](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/768b340c35c5f145940bb9966e2e6713039be32f))
+- unify card styles across the project ([4a25d8d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4a25d8df03b26f25800f882e46223bab64873b73))
+- use native Telegram popup for email preview restriction ([7aeb47f](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/7aeb47f583056d651b90db4ed7cad2d2a2aef3b1))
+- use pill-style tabs in admin user detail page ([09584fc](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/09584fc901d778453fbda437d643f4bd9c4321e3))
+- use RemnaWave icon in admin panel menu ([4034b4d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/4034b4db3932cab273636bb54bf796aff6de712c))
+- use single shared WebSocket connection and optimize build chunks ([f6854c6](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/f6854c6c3aef3ff46d7839dfe94d2ec6bf4d7d64))
+- wrap all SDK isSupported() calls in try-catch ([e5ea09d](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e5ea09dd3a13ec47ca65de93c6f5858c1fadb135))
diff --git a/docker-compose.yml b/docker-compose.yml
index 2ffd9bb..316684f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,15 +22,7 @@ services:
# Внутри контейнера nginx слушает на порту 80
- '${CABINET_PORT:-3020}:80'
healthcheck:
- test:
- [
- 'CMD',
- 'wget',
- '--no-verbose',
- '--tries=1',
- '--spider',
- 'http://localhost:80/',
- ]
+ test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:80/']
interval: 30s
timeout: 3s
retries: 3
diff --git a/eslint.config.js b/eslint.config.js
index f33a89d..6f488b5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,9 +1,9 @@
-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'
-import eslintConfigPrettier from 'eslint-config-prettier'
+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';
+import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
{
@@ -11,10 +11,7 @@ export default tseslint.config(
},
{
files: ['**/*.{ts,tsx}'],
- extends: [
- js.configs.recommended,
- ...tseslint.configs.recommended,
- ],
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
@@ -31,10 +28,7 @@ export default tseslint.config(
'react-hooks/refs': 'off',
'react-hooks/purity': 'off',
'react-hooks/variables': 'off',
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'no-empty': 'warn',
@@ -45,4 +39,4 @@ export default tseslint.config(
},
},
eslintConfigPrettier,
-)
+);
diff --git a/index.html b/index.html
index 35ef396..50ae845 100644
--- a/index.html
+++ b/index.html
@@ -1,9 +1,12 @@
-
+
-
+
@@ -11,7 +14,9 @@
Loading...
diff --git a/package-lock.json b/package-lock.json
index c3b1b0e..73d9f3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@kastov/cryptohapp": "^1.1.2",
"@lottiefiles/dotlottie-react": "^0.8.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
@@ -33,6 +34,7 @@
"framer-motion": "^12.29.2",
"i18next": "^23.7.0",
"i18next-browser-languagedetector": "^7.2.0",
+ "jsencrypt": "^3.5.4",
"ogl": "^1.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1182,6 +1184,20 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kastov/cryptohapp": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@kastov/cryptohapp/-/cryptohapp-1.1.2.tgz",
+ "integrity": "sha512-vwBLWmKJ6g8opVfvzFPZxX3U65ncXawsasqlO5PxVvpejl3MgkpveUkOMoec6gNuGgSSjjxv4ydFH0h3qz3gBw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "jsencrypt": ">=3"
+ },
+ "peerDependenciesMeta": {
+ "jsencrypt": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@lottiefiles/dotlottie-react": {
"version": "0.8.12",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.8.12.tgz",
@@ -4702,6 +4718,13 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsencrypt": {
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz",
+ "integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5705,7 +5728,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
diff --git a/package.json b/package.json
index 21eeeeb..f7bd6cb 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@kastov/cryptohapp": "^1.1.2",
"@lottiefiles/dotlottie-react": "^0.8.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
@@ -41,6 +42,7 @@
"framer-motion": "^12.29.2",
"i18next": "^23.7.0",
"i18next-browser-languagedetector": "^7.2.0",
+ "jsencrypt": "^3.5.4",
"ogl": "^1.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/postcss.config.js b/postcss.config.js
index 2e7af2b..2aa7205 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
-}
+};
diff --git a/src/App.tsx b/src/App.tsx
index 28eda68..7cd8f0f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,6 +26,7 @@ const Contests = lazy(() => import('./pages/Contests'));
const Polls = lazy(() => import('./pages/Polls'));
const Info = lazy(() => import('./pages/Info'));
const Wheel = lazy(() => import('./pages/Wheel'));
+const Connection = lazy(() => import('./pages/Connection'));
const TopUpMethodSelect = lazy(() => import('./pages/TopUpMethodSelect'));
const TopUpAmount = lazy(() => import('./pages/TopUpAmount'));
@@ -262,6 +263,16 @@ function App() {
}
/>
+
+
+
+
+
+ }
+ />
{/* Admin routes */}
;
-}
-
export const adminAppsApi = {
- // Get full app config
- getConfig: async (): Promise => {
- const response = await apiClient.get('/cabinet/admin/apps');
- return response.data;
- },
-
- // Get available platforms
- getPlatforms: async (): Promise => {
- const response = await apiClient.get('/cabinet/admin/apps/platforms');
- return response.data;
- },
-
- // Get apps for a platform
- getPlatformApps: async (platform: string): Promise => {
- const response = await apiClient.get(
- `/cabinet/admin/apps/platforms/${platform}`,
- );
- return response.data;
- },
-
- // Create a new app
- createApp: async (platform: string, app: AppDefinition): Promise => {
- const response = await apiClient.post(
- `/cabinet/admin/apps/platforms/${platform}`,
- {
- platform,
- app,
- },
- );
- return response.data;
- },
-
- // Update an app
- updateApp: async (
- platform: string,
- appId: string,
- app: AppDefinition,
- ): Promise => {
- const response = await apiClient.put(
- `/cabinet/admin/apps/platforms/${platform}/${appId}`,
- {
- app,
- },
- );
- return response.data;
- },
-
- // Delete an app
- deleteApp: async (platform: string, appId: string): Promise => {
- await apiClient.delete(`/cabinet/admin/apps/platforms/${platform}/${appId}`);
- },
-
- // Reorder apps
- reorderApps: async (platform: string, appIds: string[]): Promise => {
- await apiClient.post(`/cabinet/admin/apps/platforms/${platform}/reorder`, {
- app_ids: appIds,
- });
- },
-
- // Copy app to another platform
- copyApp: async (
- platform: string,
- appId: string,
- targetPlatform: string,
- ): Promise<{ new_id: string }> => {
- const response = await apiClient.post<{ new_id: string; target_platform: string }>(
- `/cabinet/admin/apps/platforms/${platform}/copy/${appId}?target_platform=${targetPlatform}`,
- );
- return response.data;
- },
-
- // Get branding
- getBranding: async (): Promise => {
- const response = await apiClient.get('/cabinet/admin/apps/branding');
- return response.data;
- },
-
- // Update branding
- updateBranding: async (branding: AppConfigBranding): Promise => {
- const response = await apiClient.put('/cabinet/admin/apps/branding', {
- branding,
- });
- return response.data;
- },
-
// Get RemnaWave config status
getRemnaWaveStatus: async (): Promise<{ enabled: boolean; config_uuid: string | null }> => {
const response = await apiClient.get<{ enabled: boolean; config_uuid: string | null }>(
@@ -181,6 +55,8 @@ export const adminAppsApi = {
export interface RemnawaveButton {
url: string;
text: LocalizedText;
+ type?: 'external' | 'subscriptionLink' | 'copyButton';
+ svgIconKey?: string;
}
export interface RemnawaveBlock {
@@ -196,10 +72,12 @@ export interface RemnawaveApp {
featured?: boolean;
urlScheme?: string;
isNeedBase64Encoding?: boolean;
+ svgIconKey?: string;
blocks: RemnawaveBlock[];
}
export interface RemnawavePlatform {
+ svgIconKey?: string;
apps: RemnawaveApp[];
}
@@ -236,332 +114,3 @@ export interface RemnawaveConfig {
baseTranslations?: RemnawaveBaseTranslations;
brandingSettings?: RemnawaveBrandingSettings;
}
-
-// ============== Converter Functions ==============
-
-const emptyLocalizedText = (): LocalizedText => ({
- en: '',
- ru: '',
- zh: '',
- fa: '',
- fr: '',
-});
-
-// Convert Cabinet button to RemnaWave button
-const cabinetButtonToRemnawave = (button: AppButton): RemnawaveButton => ({
- url: button.buttonLink,
- text: { ...emptyLocalizedText(), ...button.buttonText },
-});
-
-// Convert RemnaWave button to Cabinet button
-const remnawaveButtonToCabinet = (button: RemnawaveButton): AppButton => ({
- buttonLink: button.url,
- buttonText: {
- en: button.text.en || '',
- ru: button.text.ru || '',
- zh: button.text.zh,
- fa: button.text.fa,
- fr: button.text.fr,
- },
-});
-
-// Convert Cabinet app to RemnaWave app format
-export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
- const blocks: RemnawaveBlock[] = [];
-
- // Block 1: Installation
- blocks.push({
- title: { ...emptyLocalizedText(), en: 'Install App', ru: 'Установка приложения' },
- description: { ...emptyLocalizedText(), ...app.installationStep.description },
- buttons: app.installationStep.buttons?.map(cabinetButtonToRemnawave),
- svgIconKey: 'download',
- svgIconColor: '#3B82F6',
- });
-
- // Block 2 (optional): Additional before subscription
- if (
- app.additionalBeforeAddSubscriptionStep?.description?.en ||
- app.additionalBeforeAddSubscriptionStep?.description?.ru
- ) {
- blocks.push({
- title: app.additionalBeforeAddSubscriptionStep.title || {
- ...emptyLocalizedText(),
- en: 'Preparation',
- ru: 'Подготовка',
- },
- description: {
- ...emptyLocalizedText(),
- ...app.additionalBeforeAddSubscriptionStep.description,
- },
- buttons: app.additionalBeforeAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
- svgIconKey: 'settings',
- svgIconColor: '#8B5CF6',
- });
- }
-
- // Block 3: Add subscription
- blocks.push({
- title: { ...emptyLocalizedText(), en: 'Add Subscription', ru: 'Добавить подписку' },
- description: { ...emptyLocalizedText(), ...app.addSubscriptionStep.description },
- buttons: app.addSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
- svgIconKey: 'plus',
- svgIconColor: '#10B981',
- });
-
- // Block 4 (optional): Additional after subscription
- if (
- app.additionalAfterAddSubscriptionStep?.description?.en ||
- app.additionalAfterAddSubscriptionStep?.description?.ru
- ) {
- blocks.push({
- title: app.additionalAfterAddSubscriptionStep.title || {
- ...emptyLocalizedText(),
- en: 'Configuration',
- ru: 'Настройка',
- },
- description: {
- ...emptyLocalizedText(),
- ...app.additionalAfterAddSubscriptionStep.description,
- },
- buttons: app.additionalAfterAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
- svgIconKey: 'settings',
- svgIconColor: '#F59E0B',
- });
- }
-
- // Block 5: Connect and use
- blocks.push({
- title: { ...emptyLocalizedText(), en: 'Connect and Use', ru: 'Подключение и использование' },
- description: { ...emptyLocalizedText(), ...app.connectAndUseStep.description },
- buttons: app.connectAndUseStep.buttons?.map(cabinetButtonToRemnawave),
- svgIconKey: 'check',
- svgIconColor: '#22C55E',
- });
-
- return {
- name: app.name,
- featured: app.isFeatured,
- urlScheme: app.urlScheme,
- isNeedBase64Encoding: app.isNeedBase64Encoding,
- blocks,
- };
-};
-
-// Convert RemnaWave app to Cabinet app format
-export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppDefinition => {
- const blocks = app.blocks || [];
-
- // Default empty step
- const emptyStep = (): AppStep => ({
- description: emptyLocalizedText(),
- buttons: [],
- });
-
- // Try to identify blocks by their titles or position
- let installationBlock: RemnawaveBlock | undefined;
- let subscriptionBlock: RemnawaveBlock | undefined;
- let connectBlock: RemnawaveBlock | undefined;
- let beforeSubBlock: RemnawaveBlock | undefined;
- let afterSubBlock: RemnawaveBlock | undefined;
-
- // First pass: try to identify by title keywords
- for (const block of blocks) {
- const enTitle = (block.title?.en || '').toLowerCase();
- const ruTitle = (block.title?.ru || '').toLowerCase();
-
- if (enTitle.includes('install') || ruTitle.includes('установ') || ruTitle.includes('скачай')) {
- if (!installationBlock) installationBlock = block;
- } else if (
- enTitle.includes('subscription') ||
- enTitle.includes('add') ||
- ruTitle.includes('подписк') ||
- ruTitle.includes('добав')
- ) {
- if (!subscriptionBlock) subscriptionBlock = block;
- } else if (
- enTitle.includes('connect') ||
- enTitle.includes('use') ||
- ruTitle.includes('подключ') ||
- ruTitle.includes('использ')
- ) {
- if (!connectBlock) connectBlock = block;
- }
- }
-
- // Second pass: assign remaining blocks by position if not found by title
- if (!installationBlock && blocks.length > 0) {
- installationBlock = blocks[0];
- }
- if (!subscriptionBlock && blocks.length > 1) {
- subscriptionBlock = blocks.find((b) => b !== installationBlock) || blocks[1];
- }
- if (!connectBlock && blocks.length > 2) {
- connectBlock =
- blocks.find((b) => b !== installationBlock && b !== subscriptionBlock) ||
- blocks[blocks.length - 1];
- }
-
- // Assign additional blocks
- const additionalBlocks = blocks.filter(
- (b) => b !== installationBlock && b !== subscriptionBlock && b !== connectBlock,
- );
- if (additionalBlocks.length >= 1) {
- // Check if block appears before subscription
- const subIndex = blocks.indexOf(subscriptionBlock!);
- const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0]);
- if (firstAdditionalIndex < subIndex) {
- beforeSubBlock = additionalBlocks[0];
- if (additionalBlocks.length >= 2) {
- afterSubBlock = additionalBlocks[1];
- }
- } else {
- afterSubBlock = additionalBlocks[0];
- }
- }
-
- // Convert block to cabinet step
- const blockToStep = (block: RemnawaveBlock | undefined): AppStep => {
- if (!block) return emptyStep();
- return {
- description: {
- en: block.description?.en || '',
- ru: block.description?.ru || '',
- zh: block.description?.zh,
- fa: block.description?.fa,
- fr: block.description?.fr,
- },
- title: block.title
- ? {
- en: block.title.en || '',
- ru: block.title.ru || '',
- zh: block.title.zh,
- fa: block.title.fa,
- fr: block.title.fr,
- }
- : undefined,
- buttons: block.buttons?.map(remnawaveButtonToCabinet),
- };
- };
-
- return {
- id: `${app.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${platform}-${Date.now()}`,
- name: app.name,
- isFeatured: app.featured || false,
- urlScheme: app.urlScheme || '',
- isNeedBase64Encoding: app.isNeedBase64Encoding,
- installationStep: blockToStep(installationBlock),
- addSubscriptionStep: blockToStep(subscriptionBlock),
- connectAndUseStep: blockToStep(connectBlock),
- additionalBeforeAddSubscriptionStep: beforeSubBlock ? blockToStep(beforeSubBlock) : undefined,
- additionalAfterAddSubscriptionStep: afterSubBlock ? blockToStep(afterSubBlock) : undefined,
- };
-};
-
-// Export all apps from cabinet to RemnaWave format
-export const exportToRemnawaveFormat = (
- platformApps: Record,
- branding?: AppConfigBranding,
-): RemnawaveConfig => {
- const platforms: Record = {};
-
- for (const [platform, apps] of Object.entries(platformApps)) {
- platforms[platform] = {
- apps: apps.map(cabinetAppToRemnawave),
- };
- }
-
- return {
- platforms,
- svgLibrary: {
- download: {
- svgString:
- '',
- },
- plus: {
- svgString:
- '',
- },
- check: {
- svgString:
- '',
- },
- settings: {
- svgString:
- '',
- },
- },
- baseSettings: {
- isShowTutorialButton: false,
- tutorialUrl: '',
- },
- baseTranslations: {
- installApp: {
- en: 'Install App',
- ru: 'Установить приложение',
- zh: '安装应用',
- fa: 'نصب برنامه',
- fr: "Installer l'application",
- },
- addSubscription: {
- en: 'Add Subscription',
- ru: 'Добавить подписку',
- zh: '添加订阅',
- fa: 'اضافه کردن اشتراک',
- fr: 'Ajouter un abonnement',
- },
- connectAndUse: {
- en: 'Connect and Use',
- ru: 'Подключиться и использовать',
- zh: '连接并使用',
- fa: 'اتصال و استفاده',
- fr: 'Connecter et utiliser',
- },
- copyLink: {
- en: 'Copy Link',
- ru: 'Скопировать ссылку',
- zh: '复制链接',
- fa: 'کپی لینک',
- fr: 'Copier le lien',
- },
- openApp: {
- en: 'Open App',
- ru: 'Открыть приложение',
- zh: '打开应用',
- fa: 'باز کردن برنامه',
- fr: "Ouvrir l'application",
- },
- tutorial: { en: 'Tutorial', ru: 'Инструкция', zh: '教程', fa: 'آموزش', fr: 'Tutoriel' },
- close: { en: 'Close', ru: 'Закрыть', zh: '关闭', fa: 'بستن', fr: 'Fermer' },
- },
- brandingSettings: branding
- ? {
- name: branding.name,
- logoUrl: branding.logoUrl,
- supportUrl: branding.supportUrl,
- }
- : undefined,
- };
-};
-
-// Import apps from RemnaWave format to cabinet
-export const importFromRemnawaveFormat = (
- config: RemnawaveConfig,
-): { platformApps: Record; branding?: AppConfigBranding } => {
- const platformApps: Record = {};
-
- for (const [platform, platformData] of Object.entries(config.platforms || {})) {
- platformApps[platform] = (platformData.apps || []).map((app) =>
- remnawaveAppToCabinet(app, platform),
- );
- }
-
- const branding = config.brandingSettings
- ? {
- name: config.brandingSettings.name,
- logoUrl: config.brandingSettings.logoUrl,
- supportUrl: config.brandingSettings.supportUrl,
- }
- : undefined;
-
- return { platformApps, branding };
-};
diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx
deleted file mode 100644
index 7818ce3..0000000
--- a/src/components/ConnectionModal.tsx
+++ /dev/null
@@ -1,733 +0,0 @@
-import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
-import { createPortal } from 'react-dom';
-import { useTranslation } from 'react-i18next';
-import { useQuery } from '@tanstack/react-query';
-import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
-import { subscriptionApi } from '../api/subscription';
-import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
-import { useBackButton, useHaptic } from '@/platform';
-import type { AppInfo, AppConfig, LocalizedText } from '../types';
-
-interface ConnectionModalProps {
- onClose: () => void;
-}
-
-// Icons
-const CloseIcon = () => (
-
-);
-
-const CopyIcon = () => (
-
-);
-
-const CheckIcon = () => (
-
-);
-
-const LinkIcon = () => (
-
-);
-
-const ChevronIcon = () => (
-
-);
-
-const BackIcon = () => (
-
-);
-
-// App icons
-const HappIcon = () => (
-
-);
-
-const ClashMetaIcon = () => (
-
-);
-
-const ShadowrocketIcon = () => (
-
-);
-
-const StreisandIcon = () => (
-
-);
-
-const getAppIcon = (appName: string): React.ReactNode => {
- const name = appName.toLowerCase();
- if (name.includes('happ')) return ;
- if (name.includes('shadowrocket') || name.includes('rocket')) return ;
- if (name.includes('streisand')) return ;
- if (name.includes('clash') || name.includes('meta') || name.includes('verge'))
- return ;
- return 📦;
-};
-
-const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
-// eslint-disable-next-line no-script-url -- listing dangerous schemes to block them
-const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'];
-
-function isValidExternalUrl(url: string | undefined): boolean {
- if (!url) return false;
- const lowerUrl = url.toLowerCase().trim();
- if (dangerousSchemes.some((scheme) => lowerUrl.startsWith(scheme))) return false;
- return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
-}
-
-function isValidDeepLink(url: string | undefined): boolean {
- if (!url) return false;
- const lowerUrl = url.toLowerCase().trim();
- if (dangerousSchemes.some((scheme) => lowerUrl.startsWith(scheme))) return false;
- return lowerUrl.includes('://');
-}
-
-function detectPlatform(): string | null {
- if (typeof window === 'undefined' || !navigator?.userAgent) return null;
- const ua = navigator.userAgent.toLowerCase();
- if (/iphone|ipad|ipod/.test(ua)) return 'ios';
- if (/android/.test(ua)) return /tv|television/.test(ua) ? 'androidTV' : 'android';
- if (/macintosh|mac os x/.test(ua)) return 'macos';
- if (/windows/.test(ua)) return 'windows';
- if (/linux/.test(ua)) return 'linux';
- return null;
-}
-
-function useIsMobile() {
- const [isMobile, setIsMobile] = useState(() => {
- if (typeof window === 'undefined') return false;
- return window.innerWidth < 768;
- });
- useEffect(() => {
- const check = () => setIsMobile(window.innerWidth < 768);
- window.addEventListener('resize', check);
- return () => window.removeEventListener('resize', check);
- }, []);
- return isMobile;
-}
-
-export default function ConnectionModal({ onClose }: ConnectionModalProps) {
- const { t, i18n } = useTranslation();
- const [selectedApp, setSelectedApp] = useState(null);
- const [copied, setCopied] = useState(false);
- const [showAppSelector, setShowAppSelector] = useState(false);
- const [selectedPlatform, setSelectedPlatform] = useState(null);
-
- const { isTelegramWebApp, isFullscreen, safeAreaInset, contentSafeAreaInset } =
- useTelegramWebApp();
- const { impact: hapticImpact } = useHaptic();
- const isMobileScreen = useIsMobile();
- const isMobile = isMobileScreen;
- const scrollContainerRef = useRef(null);
-
- // Ref для хранения актуального обработчика BackButton (фикс мигания)
- const backButtonHandlerRef = useRef<() => void>(() => {});
- // Ref for haptic to avoid recreating handleBackButton
- const hapticRef = useRef(hapticImpact);
- hapticRef.current = hapticImpact;
-
- // Prevent scroll events from bubbling to parent/Telegram
- const handleScrollContainerWheel = useCallback((e: React.WheelEvent) => {
- const container = e.currentTarget;
- const { scrollTop, scrollHeight, clientHeight } = container;
- const isAtTop = scrollTop === 0;
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
-
- // Prevent scroll propagation when not at boundaries, or when scrolling away from boundary
- if ((!isAtTop && !isAtBottom) || (isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
- e.stopPropagation();
- }
- }, []);
-
- const safeBottom = isTelegramWebApp
- ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom)
- : 0;
- const safeTop = isTelegramWebApp
- ? Math.max(safeAreaInset.top, contentSafeAreaInset.top) + (isFullscreen ? 45 : 0)
- : 0;
-
- const {
- data: appConfig,
- isLoading,
- error,
- } = useQuery({
- queryKey: ['appConfig'],
- queryFn: () => subscriptionApi.getAppConfig(),
- });
-
- const detectedPlatform = useMemo(() => detectPlatform(), []);
-
- useEffect(() => {
- if (!appConfig?.platforms || selectedApp) return;
- let platform = detectedPlatform;
- if (!platform || !appConfig.platforms[platform]?.length) {
- platform = platformOrder.find((p) => appConfig.platforms[p]?.length > 0) || null;
- }
- if (!platform || !appConfig.platforms[platform]?.length) return;
- const apps = appConfig.platforms[platform];
- const app = apps.find((a) => a.isFeatured) || apps[0];
- if (app) setSelectedApp(app);
- }, [appConfig, detectedPlatform, selectedApp]);
-
- const handleClose = useCallback(() => {
- onClose();
- }, [onClose]);
-
- const handleBack = useCallback(() => {
- if (selectedPlatform) {
- setSelectedPlatform(null);
- } else {
- setShowAppSelector(false);
- }
- }, [selectedPlatform]);
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- e.preventDefault();
- if (showAppSelector) handleBack();
- else handleClose();
- }
- };
- document.addEventListener('keydown', handleKeyDown);
- return () => document.removeEventListener('keydown', handleKeyDown);
- }, [handleClose, handleBack, showAppSelector]);
-
- // Обновляем ref при изменении логики (без перезапуска эффекта BackButton)
- useEffect(() => {
- backButtonHandlerRef.current = showAppSelector ? handleBack : handleClose;
- }, [showAppSelector, handleBack, handleClose]);
-
- // BackButton using platform hook - always close/back, ref provides current handler
- const handleBackButton = useCallback(() => {
- hapticRef.current('light');
- backButtonHandlerRef.current();
- }, []);
-
- useBackButton(handleBackButton);
-
- useEffect(() => {
- document.body.style.overflow = 'hidden';
- return () => {
- document.body.style.overflow = '';
- };
- }, []);
-
- const getLocalizedText = (text: LocalizedText | undefined): string => {
- if (!text) return '';
- const lang = i18n.language || 'en';
- return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '';
- };
-
- const availablePlatforms = useMemo(() => {
- if (!appConfig?.platforms) return [];
- const available = platformOrder.filter((key) => appConfig.platforms[key]?.length > 0);
- if (detectedPlatform && available.includes(detectedPlatform)) {
- return [detectedPlatform, ...available.filter((p) => p !== detectedPlatform)];
- }
- return available;
- }, [appConfig, detectedPlatform]);
-
- const copySubscriptionLink = async () => {
- if (!appConfig?.subscriptionUrl) return;
- try {
- await navigator.clipboard.writeText(appConfig.subscriptionUrl);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch {
- const textarea = document.createElement('textarea');
- textarea.value = appConfig.subscriptionUrl;
- document.body.appendChild(textarea);
- textarea.select();
- document.execCommand('copy');
- document.body.removeChild(textarea);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- };
-
- const handleConnect = (app: AppInfo) => {
- if (!app.deepLink || !isValidDeepLink(app.deepLink)) return;
- const deepLink = app.deepLink;
-
- // All deep links (including custom schemes like happ://, clash://, etc.)
- // go through redirect.html. Telegram's openLink opens the redirect page
- // in an external browser, which then performs window.location.href to the
- // actual deep link URL. This works for both http(s) and custom schemes.
- const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`;
-
- try {
- sdkOpenLink(redirectUrl, { tryInstantView: false });
- return;
- } catch {
- // SDK not available, fallback
- }
- window.location.href = redirectUrl;
- };
-
- // Wrapper component
- const Wrapper = ({ children }: { children: React.ReactNode }) => {
- if (isMobile) {
- const content = (
-
- {children}
-
- );
- if (typeof document !== 'undefined') return createPortal(content, document.body);
- return content;
- }
-
- // Desktop centered - positioned higher
- return (
-
-
e.stopPropagation()}
- >
- {children}
-
-
- );
- };
-
- // Loading
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- // Error
- if (error || !appConfig) {
- return (
-
-
-
{t('common.error')}
-
-
-
- );
- }
-
- // No subscription
- if (!appConfig.hasSubscription) {
- return (
-
-
-
- {t('subscription.connection.title')}
-
-
{t('subscription.connection.noSubscription')}
-
-
-
- );
- }
-
- // App selector
- if (showAppSelector) {
- const platformNames: Record = {
- ios: 'iOS',
- android: 'Android',
- windows: 'Windows',
- macos: 'macOS',
- linux: 'Linux',
- androidTV: 'Android TV',
- appleTV: 'Apple TV',
- };
-
- const platformIcons: Record = {
- ios: (
-
- ),
- android: (
-
- ),
- windows: (
-
- ),
- macos: (
-
- ),
- linux: (
-
- ),
- androidTV: (
-
- ),
- appleTV: (
-
- ),
- };
-
- // Step 1: Platform selection
- if (!selectedPlatform) {
- return (
-
-
- {!isTelegramWebApp && (
-
- )}
-
- {t('subscription.connection.selectPlatform')}
-
-
-
- {availablePlatforms.map((platform) => {
- const apps = appConfig.platforms[platform];
- if (!apps?.length) return null;
- const isCurrentPlatform = platform === detectedPlatform;
- const appCount = apps.length;
-
- return (
-
- );
- })}
-
-
- );
- }
-
- // Step 2: App selection for chosen platform
- const apps = appConfig.platforms[selectedPlatform] || [];
- const isCurrentPlatform = selectedPlatform === detectedPlatform;
-
- return (
-
-
- {!isTelegramWebApp && (
-
- )}
-
-
- {platformNames[selectedPlatform] || selectedPlatform}
-
- {isCurrentPlatform && (
-
- {t('subscription.connection.yourDevice')}
-
- )}
-
-
-
-
- {apps.map((app) => {
- const isSelected = selectedApp?.id === app.id;
- return (
-
- );
- })}
-
-
-
- );
- }
-
- // Main view
- return (
-
-
-
-
{t('subscription.connection.title')}
- {!isTelegramWebApp && (
-
- )}
-
-
-
-
-
- {selectedApp?.installationStep && (
-
-
-
- 1
-
-
- {t('subscription.connection.installApp')}
-
-
-
- {getLocalizedText(selectedApp.installationStep.description)}
-
- {selectedApp.installationStep.buttons &&
- selectedApp.installationStep.buttons.length > 0 && (
-
- )}
-
- )}
-
- {selectedApp?.addSubscriptionStep && (
-
-
-
- 2
-
-
- {t('subscription.connection.addSubscription')}
-
-
-
- {getLocalizedText(selectedApp.addSubscriptionStep.description)}
-
-
- {selectedApp.deepLink && (
-
- )}
- {/* Copy link button - hidden when hideLink is true */}
- {!appConfig?.hideLink && (
-
- )}
-
-
- )}
-
- {selectedApp?.connectAndUseStep && (
-
-
-
- 3
-
-
- {t('subscription.connection.connectVpn')}
-
-
-
- {getLocalizedText(selectedApp.connectAndUseStep.description)}
-
-
- )}
-
-
- );
-}
diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx
index e31a59a..380d851 100644
--- a/src/components/admin/ThemeTab.tsx
+++ b/src/components/admin/ThemeTab.tsx
@@ -1,8 +1,8 @@
-import { useState } from 'react';
+import { useEffect, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { themeColorsApi } from '../../api/themeColors';
-import { DEFAULT_THEME_COLORS } from '../../types/theme';
+import { DEFAULT_THEME_COLORS, ThemeColors } from '../../types/theme';
import { ColorPicker } from '../ColorPicker';
import { applyThemeColors } from '../../hooks/useThemeColors';
import { updateEnabledThemesCache } from '../../hooks/useTheme';
@@ -10,6 +10,23 @@ import { MoonIcon, SunIcon, ChevronDownIcon } from './icons';
import { Toggle } from './Toggle';
import { THEME_PRESETS } from './constants';
+function colorsEqual(a: ThemeColors, b: ThemeColors): boolean {
+ return (
+ a.accent === b.accent &&
+ a.darkBackground === b.darkBackground &&
+ a.darkSurface === b.darkSurface &&
+ a.darkText === b.darkText &&
+ a.darkTextSecondary === b.darkTextSecondary &&
+ a.lightBackground === b.lightBackground &&
+ a.lightSurface === b.lightSurface &&
+ a.lightText === b.lightText &&
+ a.lightTextSecondary === b.lightTextSecondary &&
+ a.success === b.success &&
+ a.warning === b.warning &&
+ a.error === b.error
+ );
+}
+
export function ThemeTab() {
const { t } = useTranslation();
const queryClient = useQueryClient();
@@ -29,7 +46,7 @@ export function ThemeTab() {
};
// Queries
- const { data: themeColors } = useQuery({
+ const { data: serverColors } = useQuery({
queryKey: ['theme-colors'],
queryFn: themeColorsApi.getColors,
});
@@ -39,20 +56,120 @@ export function ThemeTab() {
queryFn: themeColorsApi.getEnabledThemes,
});
+ // Local draft state
+ const [draftColors, setDraftColors] = useState(DEFAULT_THEME_COLORS);
+ const savedColorsRef = useRef(DEFAULT_THEME_COLORS);
+
+ // Sync server data into draft and saved snapshot when it arrives
+ useEffect(() => {
+ if (serverColors) {
+ const colors: ThemeColors = {
+ accent: serverColors.accent,
+ darkBackground: serverColors.darkBackground,
+ darkSurface: serverColors.darkSurface,
+ darkText: serverColors.darkText,
+ darkTextSecondary: serverColors.darkTextSecondary,
+ lightBackground: serverColors.lightBackground,
+ lightSurface: serverColors.lightSurface,
+ lightText: serverColors.lightText,
+ lightTextSecondary: serverColors.lightTextSecondary,
+ success: serverColors.success,
+ warning: serverColors.warning,
+ error: serverColors.error,
+ };
+ // Only sync if saved snapshot matches current draft (no unsaved changes)
+ if (
+ colorsEqual(savedColorsRef.current, draftColors) ||
+ colorsEqual(savedColorsRef.current, DEFAULT_THEME_COLORS)
+ ) {
+ setDraftColors(colors);
+ }
+ savedColorsRef.current = colors;
+ }
+ }, [serverColors]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const hasUnsavedChanges = !colorsEqual(draftColors, savedColorsRef.current);
+
+ // Update a single color in the draft and apply preview instantly
+ const updateDraftColor = useCallback(
+ (key: keyof ThemeColors, value: string) => {
+ setDraftColors((prev) => {
+ const next = { ...prev, [key]: value };
+ applyThemeColors(next);
+ queryClient.setQueryData(['theme-colors'], next);
+ return next;
+ });
+ },
+ [queryClient],
+ );
+
+ // Apply a full preset to draft
+ const applyPreset = useCallback(
+ (colors: Partial) => {
+ setDraftColors((prev) => {
+ const next = { ...prev, ...colors };
+ applyThemeColors(next);
+ queryClient.setQueryData(['theme-colors'], next);
+ return next;
+ });
+ },
+ [queryClient],
+ );
+
+ // Cancel: revert draft to saved
+ const handleCancel = useCallback(() => {
+ const saved = savedColorsRef.current;
+ setDraftColors(saved);
+ applyThemeColors(saved);
+ queryClient.setQueryData(['theme-colors'], saved);
+ }, [queryClient]);
+
// Mutations
const updateColorsMutation = useMutation({
mutationFn: themeColorsApi.updateColors,
onSuccess: (data) => {
- applyThemeColors(data);
- queryClient.invalidateQueries({ queryKey: ['theme-colors'] });
+ const colors: ThemeColors = {
+ accent: data.accent,
+ darkBackground: data.darkBackground,
+ darkSurface: data.darkSurface,
+ darkText: data.darkText,
+ darkTextSecondary: data.darkTextSecondary,
+ lightBackground: data.lightBackground,
+ lightSurface: data.lightSurface,
+ lightText: data.lightText,
+ lightTextSecondary: data.lightTextSecondary,
+ success: data.success,
+ warning: data.warning,
+ error: data.error,
+ };
+ savedColorsRef.current = colors;
+ setDraftColors(colors);
+ applyThemeColors(colors);
+ queryClient.setQueryData(['theme-colors'], data);
},
});
const resetColorsMutation = useMutation({
mutationFn: themeColorsApi.resetColors,
onSuccess: (data) => {
- applyThemeColors(data);
- queryClient.invalidateQueries({ queryKey: ['theme-colors'] });
+ const colors: ThemeColors = {
+ accent: data.accent,
+ darkBackground: data.darkBackground,
+ darkSurface: data.darkSurface,
+ darkText: data.darkText,
+ darkTextSecondary: data.darkTextSecondary,
+ lightBackground: data.lightBackground,
+ lightSurface: data.lightSurface,
+ lightText: data.lightText,
+ lightTextSecondary: data.lightTextSecondary,
+ success: data.success,
+ warning: data.warning,
+ error: data.error,
+ };
+ savedColorsRef.current = colors;
+ setDraftColors(colors);
+ applyThemeColors(colors);
+ queryClient.setQueryData(['theme-colors'], data);
},
});
@@ -130,8 +247,7 @@ export function ThemeTab() {
{THEME_PRESETS.map((preset) => (