Commit Graph

1910 Commits

Author SHA1 Message Date
c0mrade
47119c4c1f Merge pull request #456 from LLC-INCY/feature/pages-and-backgrounds
feat: per-method quick amounts, system pages admin, animated backgrounds overhaul
2026-06-11 13:33:02 +03:00
Boris Kovalskii
9e6372afba fix: address review feedback on admin pages and backgrounds
- guard config.default_quick_amounts with ?? [] so the edit page survives a
  cabinet deployed ahead of the bot
- roll back the first faq reorder request when the second fails so both
  questions don't end up with the same display_order
- stop overriding the aurora palette in render for the legacy seed colors
- add the missing legalPages / displayMode / quickAmounts keys to fa and zh
2026-06-11 20:00:52 +10:00
Boris Kovalskii
acd0d7c69e fix(backgrounds): keep constellation particles in bounds on resize 2026-06-11 11:14:57 +10:00
Boris Kovalskii
510e4bd738 feat(backgrounds): add constellation background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
71160c6700 feat(backgrounds): add liquid-gradient background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
ab91c86f81 feat(backgrounds): add matrix-rain background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
3df2ef0f39 fix(backgrounds): live speed and size updates for fireflies 2026-06-11 11:14:57 +10:00
Boris Kovalskii
3742f2f88d feat(backgrounds): add starfield background with depth projection 2026-06-11 11:14:57 +10:00
Boris Kovalskii
e3dbc4e663 feat(backgrounds): add snowfall background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
1d00ca97e5 feat(backgrounds): add fireflies background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
caf499a824 fix(backgrounds): legacy config compatibility and locale sync 2026-06-11 11:14:57 +10:00
Boris Kovalskii
725bf8df39 fix(backgrounds): configurable wave colors and boxes grid color with explicit multicolor mode 2026-06-11 11:14:56 +10:00
Boris Kovalskii
acbccc8af8 fix(backgrounds): make beams and beams-collision colors configurable 2026-06-11 11:14:56 +10:00
Boris Kovalskii
f1f4281e11 fix(backgrounds): expose remaining hardcoded colors in shooting-stars, meteors, gradient-animation 2026-06-11 11:14:56 +10:00
Boris Kovalskii
66c08b5d6d fix(backgrounds): read aurora gradient colors and speed from settings 2026-06-11 11:14:56 +10:00
Boris Kovalskii
515c3d73b8 fix(admin): harden legal pages editor state handling 2026-06-11 11:14:56 +10:00
Boris Kovalskii
39758a18b0 feat: hide info tabs by display mode visibility 2026-06-11 11:14:56 +10:00
Boris Kovalskii
0d6a85a4f9 feat: add system pages admin section 2026-06-11 11:14:56 +10:00
Boris Kovalskii
04cbbb5e51 feat: add display mode selector to info page editor 2026-06-11 11:14:56 +10:00
Boris Kovalskii
e979aa8863 feat: add admin legal pages api client and display mode types 2026-06-11 11:14:56 +10:00
Boris Kovalskii
92198854b2 fix(balance): align quick amounts edge cases with bot behavior 2026-06-11 11:14:56 +10:00
Boris Kovalskii
bb8b823b37 feat(admin): quick amounts editor in payment method settings 2026-06-11 11:14:56 +10:00
Boris Kovalskii
25784dbe18 feat(balance): per-method quick amounts on top-up page 2026-06-11 11:14:56 +10:00
c0mrade
8b2501d908 feat(admin): show common placeholders group in email template editor
Companion to the bot-side change: the variables hint now renders two
groups — type-specific vars and common ones ({service_name},
{cabinet_url}, {support_username}, {username}, {email}, {date}) that
work in every template. The block is visible even for types with no
vars of their own (previously it was hidden entirely, looking like
placeholders didn't exist). Common chips insert at cursor the same way.
2026-06-10 23:37:31 +03:00
c0mrade
81581f5c9b fix: guard decodeURI(Component) against lone-surrogate crash (#667225)
'String contained an illegal UTF-16 sequence' on first Mini App / site
open for users with a truncated emoji in their Telegram name.

The existing surrogate guard only patched the ENCODE direction
(encodeURI/encodeURIComponent). But Telegram percent-encodes such names
as CESU-8 (%ED%A0%BD) in tgWebAppData, and decodeURI/decodeURIComponent
throw on those bytes in every engine (JSC wording is the reported one) —
any decode of init-data-derived strings crashed into the ErrorBoundary.

Not valibot: valibot 1.0.0 has no UTF-16 well-formedness validation
(verified against the installed package), and upstream pins it exactly,
so there is nothing to bump.

decodeURI/decodeURIComponent now try the native decoder first and fall
back to lenient WHATWG-style UTF-8 decoding with U+FFFD replacement —
the same semantics URLSearchParams applies to the same bytes. Decoded
values still flow through existing validation (getSafeRedirectPath etc),
so nothing becomes less strict.
2026-06-10 22:53:25 +03:00
c0mrade
ac75a806d9 fix(admin): rework email template editor (#667043)
Companion to the bot-side fix: defaults now arrive with {placeholders}
instead of baked-in sample values (example.com links).

- inline preview tab (iframe srcDoc) — works on mobile and in Telegram;
  the separate desktop-only preview page and its route are removed
- preview/test render the CURRENT editor content with sample values
- test email: recipient input + detailed backend error messages
- variable chips insert at cursor instead of copying to clipboard
- 'insert default template' button to migrate broken saved overrides
- fa added to language tabs
2026-06-10 22:53:08 +03:00
c0mrade
d7f7bc7c17 fix(cabinet): Lava top-up return route + Telegram-unavailable card overflow
- Lava returns to a path-based result URL (/balance/top-up/result/:method) because Lava
  Business rejects query strings in success/fail URLs. Add the route and read the method
  from the path param as a fallback (alongside ?method=) so external-browser redirects
  still poll the right payment. Pairs with the backend fix.
- ConnectedAccounts: the "Привязка Telegram временно недоступна" message overflowed the
  card (it sat in the non-shrinking action column and never wrapped). Constrain its width
  and wrap it (max-w + break-words) so it stays inside the card. Verified by rendering.

Both reported in the Bedolaga bug topic.
2026-06-10 16:44:46 +03:00
c0mrade
16fad9f4fe fix(cabinet): recover "Сервис недоступен" false-positive + top-up fixes
- 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.
2026-06-10 16:15:55 +03:00
c0mrade
75a570b862 docs(payments): correct open_url_direct toggle hint to match real behavior (#654272)
The hint claimed the provider opens 'inside the MiniApp/tab' — that is the broken behavior that caused #654272 (a same-WebView navigation to the provider can't hand off to a bank app / SBP). After the fix the link opens in the EXTERNAL browser inside Telegram regardless of the toggle. Rewrote the hint in all four locales: ON = open immediately (no button panel), OFF = show an 'Open payment' button; both open externally in Telegram so bank-app/SBP payments work; Stars/CryptoBot (t.me/ links) always open natively and ignore the flag.

Note: the external-browser open is unconditional (both toggle states, all methods incl. WATA) — the toggle only controls auto-open vs a button tap.
2026-06-08 15:51:06 +03:00
c0mrade
ce5737fcbc fix(payments): open_url_direct payment URL opens externally in Telegram (#654272)
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.
2026-06-08 14:21:45 +03:00
c0mrade
325e221e32 fix(connection): stop ERR_UNKNOWN_URL_SCHEME when opening app deep links on mobile
Telegram bug #654272: opening the connection app link (immediate-open / connect button)
showed a full-page net::ERR_UNKNOWN_URL_SCHEME on Android and silently failed on iOS, while
working on desktop.

Cause: a programmatic top-level navigation to a custom scheme (happ://, v2rayng://, …) via
window.location.href is rendered as a full-page error inside in-app WebViews (Telegram/Yandex)
on Android and does nothing on iOS — also wiping the fallback UI.

Add openAppScheme(): http(s) navigate normally; custom schemes launch via a hidden, contained
iframe so a failed launch never replaces the page — the app opens if installed, otherwise the
manual 'Open app' link stays usable (a user tap is the reliable trigger). Applied in
DeepLinkRedirect.tsx, Connection.tsx and the static public/miniapp/redirect.html (which now
also surfaces the manual button immediately instead of after 2s).
2026-06-08 13:11:19 +03:00
c0mrade
d37872fd4f fix(cabinet): harden global encoders against lone UTF-16 surrogates
A lone (unpaired) UTF-16 surrogate — a truncated emoji in a backend name/remark
embedded in a subscription/connection URL — makes encodeURI/encodeURIComponent throw
a URIError on iOS WebKit (V8: 'URI malformed'; Safari/JSC: 'String contained an
illegal UTF-16 sequence'). qrcode.react calls encodeURI internally, so such a value
crashed the QR render and tripped the page-level ErrorBoundary ('Something went
wrong / String contained an illegal UTF-16 sequence / Try again').

Rather than wrap each (current and future) call site — and third-party libs we can't
edit — sanitise at the single chokepoint: patch the global encodeURI/encodeURIComponent
at bootstrap to replace lone surrogates with U+FFFD (same remedy as toWellFormed()).
Fail-safe, not fail-broken: verified byte-identical output for all well-formed strings
(URLs unaffected) and no-throw on lone surrogates for the real call patterns (qrcode.react
and btoa(unescape(encodeURIComponent(...)))). A fast path skips the rewrite when no
surrogate code units are present, so the hot path (every request URL) is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-08 11:11:56 +03:00
c0mrade
35428cc27d fix(promocode): correct activation UX, error mapping and validity dates
Map promocode errors by stable backend code (detail.code) instead of brittle English
substring matching, so previously-unmapped codes (active_discount_exists, daily_limit,
no_subscription_for_days, trial_*, etc.) show the right message instead of 'server error';
added the missing keys to all 4 locales.

valid_until now anchors to end-of-day in the admin's LOCAL timezone on both create AND
edit read-back, so codes no longer expire at the start of the day and editing no longer
drifts the date forward for negative-offset admins.

Allow 0-hour discount ('until first purchase'). Route user-facing discount deactivation
to the endpoint that rolls back promocode usage when the discount came from a promocode,
and parse the structured {code,message} error.
2026-06-08 00:23:31 +03:00
c0mrade
1a3236f650 fix(cabinet): equal-height StatCards so dashboard balance/referral align
In a grid, the wrapping <Link> stretched to the row height but the
StatCard inside only took content height, so the referral card (which has
a subValue line) rendered taller than the balance card ('один толще
другого'). Make StatCard fill its cell with h-full and stretch the
StatsGrid links. Verified by rendering both cards against the built CSS:
balance 86px->94px, now equal to referral's 94px.
2026-06-06 21:44:07 +03:00
c0mrade
3b48abbb7b refactor(cabinet): unify all statistics onto the shared StatCard
Bring every stat strip across the admin panel and user cabinet onto the
canonical StatCard (icon chip + label-on-top + tone-coloured value),
matching the sales/Remnawave style the rest of the app already used.

- Convert ~20 hand-rolled stat strips across 21 pages/components
- Remove 4 divergent local StatCard components (Dashboard, Users,
  BanSystem, Payments) and migrate Remnawave's local StatCard (33 cards)
- Extend StatCard with optional subValue, loading skeleton, trailing slot,
  and icon-size normalisation (all backward-compatible)
- StatsGrid nav tiles now use StatCard while keeping link/chevron/loading
- Preserve all data, i18n keys, conditionals and interactive filters;
  fix the dead 'info' colour by mapping it to accent
2026-06-06 21:27:57 +03:00
c0mrade
9fc681b56d fix(desktop-header): stop layout jumping via 3-column grid
The header used flex with the capsule centered by mx-auto, so the capsule's
auto-margins recomputed whenever anything in the row changed (and compounded
with framer-motion's shared-layout measurement on route change) — making the
whole row, incl. the language button, twitch when navigating (notably into
/admin).

Switch the inner row to grid grid-cols-[1fr_auto_1fr]: the capsule is anchored
to the true viewport center by the symmetric side columns, and the actions are
pinned to the right column (justify-self-end). A width change in one zone can no
longer shift another, so nothing jumps.
2026-06-05 17:29:55 +03:00
c0mrade
7de9b3b51b feat(desktop-header): group nav into a centered segmented capsule
Wrap the always-visible nav items in a single rounded track (segmented-control
look): rounded-full border + subtle bg + backdrop blur, centered between the
logo and the right-side actions via mx-auto (fills the previously empty middle).

- All items stay visible with icon + label — no scroll, no shrink, no collapse,
  no hover-reveal. Purely a visual grouping.
- Items become rounded-full pills; the framer-motion active indicator slides
  within the capsule and is lifted above the track (bg + ring + shadow).
- Admin link stays inside the capsule after a thin divider, warning-tinted.
- Right-side actions sit at the edge (dropped ml-auto since nav now centers).
2026-06-05 17:21:21 +03:00
c0mrade
864aeaf600 fix(desktop-header): full-width row so all nav items fit (no overflow/overlap)
The nav was centered inside a max-w-6xl grid column; 8 icon+label items were
wider than that column, so the centered overflow spilled LEFT over the logo
(ZeroPing's tail poked out from under 'Главная'). Switch to a full-width flex
row — logo (shrink-0) | nav | controls (shrink-0, pinned right via ml-auto) —
widen the container cap to 1600px, slightly compact the items, and drop the
invisible theme toggle from layout when it's not toggleable. Icons + labels
all stay visible: no scroll, no shrink, no overlap.
2026-06-05 00:32:32 +03:00
c0mrade
e4cd975d9c refactor(desktop-header): elegant labeled nav, drop hover-reveal + dead sidebar
The desktop top-bar nav rendered icon-only buttons whose label only slid out
on hover (max-w-0 -> group-hover:max-w-40) — mystery-meat, jumpy, hard to
scan. Replace with always-visible 'icon + label' links and a single
framer-motion pill (shared layoutId) that smoothly slides to the active item;
compact spacing, refined active/hover states.

Also delete DesktopSidebar.tsx — dead code since the cabinet's inception
(never imported anywhere, only re-exported from the barrel).
2026-06-05 00:16:01 +03:00
c0mrade
800daf7506 fix(cabinet): sweep mobile flex-overflow across all pages + components
Full scan of every page/component for the Connected-Accounts overflow class
(long user data with no truncation pushing siblings off the card on mobile),
adversarially verified. Applied min-w-0 + truncate (break-all for copyable
values, break-words for titles) on text columns and shrink-0 on fixed
siblings across ~23 files: AppHeader, dashboard/SubscriptionCardActive,
subscription purchase/sheets (TariffPurchaseForm, ClassicPurchaseWizard,
SwitchTariffSheet), SuccessNotificationModal, PurchaseSuccess, Wheel sub
picker, ReferralNetwork UserDetailPanel, and the admin pages (AnalyticsTab,
BrandingTab, SubscriptionSubRow, user-detail Info/Referrals tabs,
ApplicationReview, PartnerCampaignAssign, Partners, PromocodeStats,
PromoOfferSend, RemnawaveSquadDetail, TariffCreate, Tickets, Withdrawals).
2026-06-04 23:32:59 +03:00
c0mrade
7383505a12 fix(connected-accounts): stack status + unlink so the full ID shows on mobile
Truncating the Google/Discord sub id with an ellipsis hid it. Stack the
status badge above the unlink button (flex-col) on the right so the right
column is narrow and the identifier gets the room to render in full;
truncate stays only as a safety net for extreme values.
2026-06-04 23:17:57 +03:00
c0mrade
63863a048b fix(cabinet): stop long user data overflowing flex rows on mobile
Same flex-overflow class as the Connected-Accounts card — a long
user-controlled string with no truncation pushes its sibling badge/button
out of the card on narrow screens. Found via a scan of the high-risk
screens; fixed with min-w-0 + truncate/break-words on the text column and
shrink-0 on the fixed siblings:
- Balance: promocode-target tariff name vs days-left badge
- ReferralNetwork CampaignDetailPanel: start_parameter + top-referrer username
- ServerManagementSheet: country/server name vs flag emoji
- Contests / Polls: contest/poll title vs prize/reward badge
2026-06-04 23:03:42 +03:00
c0mrade
c39c164d51 fix(connected-accounts): keep status/unlink inside the card on long IDs
A long Google sub (numeric, ~21 digits) pushed 'Привязан' and 'Отвязать'
past the right edge of the account card on mobile. Classic flex overflow:
the left identifier column had no min-w-0, the identifier <p> no truncate,
and the right actions no shrink-0. Add min-w-0 + truncate to the text column
and shrink-0 to the actions so the id ellipsizes and the controls stay put.
2026-06-04 22:53:51 +03:00
c0mrade
e4b3c8640b fix(admin-wheel): unify prize-order banner buttons + drop hardcoded i18n fallbacks
The unsaved-order banner used ad-hoc inline button classes (rounded-lg
bg-warning-500 / border bg-dark-700) instead of the shared btn-primary /
btn-secondary, plus t(...) || 'русский текст' fallbacks that would leak
Russian to non-RU locales. The keys exist in all four locales, so the
fallbacks were dead code — switched to the shared buttons and plain keys.
2026-06-04 22:41:55 +03:00
c0mrade
5de43c1071 fix(admin-wheel): unify statistics rendering + fill config UI gaps
Statistics tab now uses the shared StatCard + BreakdownList (ranked bars +
share %) and the currency-aware formatter — matching the rest of the admin
stats instead of ad-hoc 'card p-4 text-center' / text-3xl tiles — and gains
proper loading, error and empty states.

Also surface backend-supported config the form couldn't reach (forcing DB edits):
- promo_validity_days (model + schema-bounded 1-365) is now editable in Settings.
- per-prize manual_probability override (0-1) now has an input in the prize form.
2026-06-04 22:33:12 +03:00
c0mrade
73ee42a0ef fix(wheel): stop showing fake wins in the browser
After a Stars payment the wheel matched its landing sector to the won prize
by display_name/emoji and, on any miss, span to Math.random()*360 — which
often pointed at a winning slot. The poll result also carried the SPIN id in
prize_id (not the WheelPrize id), so it never matched and always hit that
random fallback. In a backgrounded browser tab the poll often times out,
so the wheel 'landed' on месяц/50₽ while the real result was Ничего; the
Mini App was fine because it gets the result inline.

- Match the sector by prize_id (exact), with name/emoji only as a defensive
  fallback for old payloads.
- Never land on a random angle: when the prize can't be resolved (poll
  timeout/error) land on the neutral 'Nothing' sector instead.
- Carry the real WheelPrize id (now exposed by the bot history endpoint)
  through the poll result.
2026-06-04 22:15:43 +03:00
c0mrade
3224320095 feat(connected-accounts): email-merge confirmation via emailed one-time code
When linking an email that belongs to another account, the backend now mails a
one-time code to it (merge_verification='email_code') instead of asking for that
account's password. Add a 6-digit code step: on the email_code response show the
code input, verify via authApi.verifyEmailMerge → on success redirect to /merge.
Preview/execute already go through apiClient (JWT), so they satisfy the new
initiator-bound merge endpoints. i18n in ru/en/zh/fa.
2026-06-04 19:06:54 +03:00
c0mrade
c6fc167d80 fix(security): use signed media tokens for ticket attachments
Backend now requires a signed, expiring token to download media (a raw file_id
404s). Carry the per-attachment `token` (and the legacy `media_token`) from the
ticket response through MessageMediaGrid and append it in ticketsApi.getMediaUrl,
so attachments keep rendering while leaked URLs expire. Must deploy together with
the backend change.
2026-06-04 17:45:43 +03:00
c0mrade
b4f066942a fix(security): validate the deep-link url in miniapp/redirect.html (open redirect + DOM-XSS)
public/miniapp/redirect.html read the `url` query param and assigned it to both
the manual <a href> and window.location.href with no validation. Served on the
cabinet's own origin, this was an open redirect (phishing) and — in Telegram /
WebView contexts that execute javascript: URIs in top-level navigation — a
DOM-XSS able to exfiltrate the refresh_token from localStorage (account takeover).

Add isSafeAppLink(): only a custom app deep link passes (scheme://, excluding
http/https/file/blob/about/intent/content and the javascript/data/vbscript
script schemes); anything else renders the no-URL state instead of navigating or
setting href. Mirrors the validation already in src/pages/DeepLinkRedirect.tsx.
2026-06-04 16:54:50 +03:00
c0mrade
7d31dc3ffa fix(connected-accounts): guide the user to enter the existing account's password when merging by email
The backend account-takeover fix now requires the existing account's password
before merging an email that already belongs to another account. Show a clear
message (profile.emailMergePasswordRequired) on that response instead of the
generic "already registered" dead-end, so the legitimate owner knows to enter
that account's password (in both password fields) to complete the merge. i18n
in ru/en/zh/fa.
2026-06-04 14:06:45 +03:00