- health probe: tolerant timeout (12s) + retry before flagging the backend down. A
hardcoded 5s probe racing auth bootstrap falsely showed ServiceUnavailableScreen on
slow devices / cold mobile connections while the 30s API requests would have
succeeded ("works on one device, not another on the same Wi-Fi"). The recovery poll
self-reschedules with the tolerant timeout so slow devices auto-recover.
- TopUpAmount: fetch payment methods with a real query (fixes the infinite spinner on a
cold cache / browser-back) and use canonical RUB for quick-amount chips so FX rounding
can't reject a min-amount selection in non-RUB locales.
- settings UI: render secret values as a masked password input (pairs with the backend
secret masking); leaving the field empty keeps the stored secret.
- deps: npm audit fix (18 -> 5 advisories).
Also bundles in-progress settings env-lock UI work.
The reported #654272 case ('функция немедленного открытия ссылки', reproduced on RollyPay AND YooKassa, fine on desktop) is the open_url_direct flow — not the connection deep-link opener fixed in 325e221.
When a payment method has open_url_direct, the cabinet did window.location.href = payment_url INSIDE the Telegram in-app WebView. SBP/RollyPay/YooKassa pages then hand off to a bank app via a custom scheme, which the WebView can't open: Android shows net::ERR_UNKNOWN_URL_SCHEME, iOS opens nothing ('приложение не определяется'); link generation logs fine. Desktop works because it's a real browser.
Add openPaymentUrl(): in Telegram open via openLink (external browser — the OS hands off to the bank app, return_url brings the user back); on web keep same-tab navigation (no popup blocker). Applied to TopUpAmount (top-up) and GiftSubscription (gift purchase). QuickPurchase is a web landing page (no platform abstraction) and is unaffected.
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77
files with the central Phosphor (react-icons/pi) components from
@/components/icons, preserving each icon's size classes and colour (dynamic
stroke colours via the parent's currentColor, RefreshIcon's spinning state,
conditional rotate-180 chevrons).
Verified: tsc + vite build + eslint clean; an adversarial diff review of all
changed files found the replacements correct (70 files clean, 0 blockers).
Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate
non-icons (brand/provider logos, loading spinners & framer-motion animations,
charts/sparklines, background decoration) plus a few ambiguous glyphs.
Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).
- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
(checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
default differed (211 call sites)
TelegramRedirect already had a local getSafeRedirectUrl helper that
collapsed protocol-relative URLs, absolute URLs, exotic schemes, and
URL-encoded forms down to '/'. TopUpAmount.handleSuccess was navigating
straight to a user-supplied returnTo query param without that filter —
not externally exploitable through react-router's navigate() (it doesn't
trigger an external nav), but a crafted link could produce ugly path
artefacts ('?returnTo=https://evil.com' would land the user at
/balance/top-up/<method>/https://evil.com).
Hoist the helper to src/utils/safeRedirect.ts, rename to
getSafeRedirectPath, reuse it in TelegramRedirect, and wrap TopUpAmount's
returnTo through it before navigate().
Replace gradient CTAs that fade between same-token shades with flat backgrounds
(TopUpAmount accent submit, ChannelSubscriptionScreen single-channel button, three
SuccessNotificationModal action buttons). Stars yellow→orange gradient kept as a
Telegram brand cue; decorative card-header gradients left as visual identity.
Post-review nits:
* AdminPaymentMethodEdit.tsx: `config.open_url_direct ?? false` when seeding
state — defends against stale backend rendering (e.g. cache invalidation
race before migration applies)
* Same file: aria-label now uses the localized `admin.paymentMethods.openUrlDirect`
translation key instead of hardcoded English
* TopUpAmount.tsx: case-insensitive guard for Telegram deep-link URLs —
`https://t.me/`, `http://t.me/`, and `tg://` all match regardless of provider
casing quirks. Lowercase normalization done once before the comparisons.
User asked for the gift-style seamless flow on balance top-up: provider
checkout opens inside Telegram MiniApp WebView without a click-to-open link
panel. Made it an admin per-method toggle so it can be enabled selectively.
src/pages/TopUpAmount.tsx — on topup mutation success:
* Move saveTopUpPendingInfo to BEFORE any redirect so /balance/top-up/result
can still pick up the pending payment after the provider's return_url fires
* If method?.open_url_direct === true AND the URL is not a t.me/ deep link
(Stars/CryptoBot), window.location.href = redirectUrl and return early
* Otherwise fall through to the existing setPaymentUrl panel — preserves
current behavior for methods without the flag enabled
The t.me/ guard is important: window.location.href to a Telegram deep link
inside a MiniApp WebView is unreliable (native shell cant always intercept).
Those URLs continue to go through openTelegramLink / openInvoice in the panel
path. Stars never reaches topUpMutation.onSuccess anyway (handled by the
separate starsPaymentMutation); the guard is defense-in-depth for CryptoBot
and any future t.me-deep-link providers.
src/pages/AdminPaymentMethodEdit.tsx — added Open URL directly toggle with
the same slider styling as the is_enabled toggle. Defaults to off.
src/types/index.ts
* PaymentMethod.open_url_direct?: boolean (user-facing)
* PaymentMethodConfig.open_url_direct: boolean (admin shape)
Translations added for ru/en/zh/fa with a hint clarifying behavior and the
t.me/ exemption.
User report: a user in EN locale paid 1.65 USD for a 150₽ subscription, but the
bot received 14960 kopeks (149.60₽) — short by 40 kopeks, blocking the
subscription purchase.
Root cause: TopUpAmount.tsx displays the prefilled RUB amount converted via
`.toFixed(2)`. With an exchange rate like 90.66 RUB/USD, 150₽ → 1.6545 USD →
displayed "1.65" (rounded down by 0.0045 USD ≈ 0.4 RUB). When the user submits
without editing, `convertToRub("1.65")` runs again and returns 1.65 × 90.66 =
149.589₽ — less than the 150₽ the user is trying to pay. Math.round/Math.ceil
on this only handles sub-kopek IEEE-754 fractions, not the deeper FX display-
rounding direction.
Fix: when the user does not edit the prefilled amount (`amount === initialDisplayAmount`)
and `initialAmountRubles > 0` is known, bypass the FX round-trip and send
`Math.round(initialAmountRubles * 100)` directly. Math.ceil for non-RUB targets
when the user does type a custom amount still helps with sub-kopek fractions.
`?amount=150` from a renew CTA → "1.65 USD" displayed → user clicks pay → bot
receives 15000 kopeks instead of 14960. Subscription renews as expected.
* Make SBP default and first top-up option (#389)
* fix(connection): happ cryptolink flow + ui fixes (#385)
* fix(connection): respect happ_cryptolink mode
- read connect_mode from connection-link endpoint
- auto-redirect to happ cryptolink flow when mode is HAPP_CRYPTOLINK
- keep installation guide behavior for other modes
* docs(readme): fix source build step 2
- use compose service name for build/create commands
- copy static files from created compose container
- remove compose container via docker compose rm
* fix(connection): handle mode and ws path
- treat any non-guide connect mode as direct happ redirect
- wait for connection-link response before rendering installation guide
- build websocket URL from VITE_API_URL (supports /api and absolute URLs)
* docs(readme): fix docker cp static path
- copy /usr/share/nginx/html contents via html/.
- prevent nested /srv/cabinet/html deployment and 404 on root
* fix(connection): keep guide, use happ links
- restore guide page behavior for /connection (no auto redirect)
- use happ cryptolink URL in /connection/qr when happ crypt mode is active
- replace subscription page connection URL with happ cryptolink from connection-link API
* fix(connection): avoid wrong redirect flow
- force happ scheme deeplink in HAPP_CRYPTOLINK mode for guide connect buttons
- use cabinet redirect page only inside Telegram Mini App
- open deeplink directly in regular browsers
* fix(connection): enforce happ cryptolink
- prioritize backend crypt deeplink fields in HAPP_CRYPTOLINK mode
- fallback to local crypt4/crypt3 generation from subscription URL when backend returns plain happ://sub...
- apply same resolution for guide open action, qr page source and subscription page link display/copy
* fix(subscription): truncate long connect link
- render connection URL in a single line with ellipsis on /subscriptions
- preserve full URL in tooltip for easier manual copy
- keep copy button behavior unchanged
* fix(connection): build cryptolink from happ sub
- allow cryptolink fallback generation from happ://sub... URLs in addition to http(s)
- prevent plain happ://sub links from leaking into UI in HAPP_CRYPTOLINK mode
* fix: allow saving 0% period discount in promo groups
Changed condition from percent > 0 to percent >= 0 so that
admins can explicitly set 0% discount for a period. Previously
0% entries were silently dropped and not sent to the backend.
* fix: always send period_discounts in promo group updates
When all period discounts were removed, the frontend sent undefined
(field absent from JSON), so the backend never cleared them.
Now always sends the field - empty object {} to clear, or populated
object to update.
---------
Co-authored-by: zavul0nn <34007368+zavul0nn@users.noreply.github.com>
Co-authored-by: Dxnil <62987903+D4nilKO@users.noreply.github.com>
- Reset isKeyboardOpen on route change so bottom nav always restores
- Disable autoFocus on payment input in Telegram Mini App
- Skip auto-focus on Profile email inputs in Telegram
Replace toast-based payment return with animated result page showing
pending/success/failed/timeout states. Extract shared Spinner,
AnimatedCheckmark and AnimatedCrossmark components. Add sessionStorage
persistence with validation and TTL for cross-redirect payment data.
Unify ProtectedRoute with withLayout prop, add aria-live accessibility.
- Add /balance/top-up route for payment method selection
- Add /balance/top-up/:methodId route for amount entry and payment
- Remove TopUpModal component (619 lines)
- Simplify InsufficientBalancePrompt to use navigate instead of modals
- Support ?amount and ?returnTo query params for cross-page data flow
- Fix nav isActive to highlight Balance on sub-routes
- Remove unused MainButton platform abstraction
- Increase toast accent opacity for better visibility