Commit Graph

780 Commits

Author SHA1 Message Date
c0mrade
576b4d5601 fix(admin): surface clipboard copy failures instead of swallowing
Two admin paths previously had empty catch blocks around copyToClipboard:
the user-detail copy helper and AdminLandings handleCopyUrl. The adapter
already attempts the legacy execCommand fallback, so reaching the catch
means the operation truly failed — staying silent left admins thinking
their click worked when it hadn't.

Both now fire notify.error so the failure is visible.
2026-05-26 13:48:10 +03:00
c0mrade
bc34e65aac fix(admin-bulk-actions): trap focus + label modal dialog (a11y)
Bulk-action modal had role=dialog on the overlay but no focus trap,
no aria-labelledby, no scroll lock, and an ad-hoc Escape listener
that conflicted with anything else listening to document keydown.

Move role=dialog/aria-modal onto the focus-trapped content element,
wire aria-labelledby to the h3 title, and replace the ad-hoc
keydown effect with useFocusTrap (Tab cycle + Esc + body scroll
lock + focus restore on close). Loading state suppresses Esc but
keeps focus trapped, so Tab still cycles inside the progress view.
2026-05-26 13:44:46 +03:00
c0mrade
7817243253 fix(admin-bulk-actions): plug stale-closure bug in columns useMemo
Header checkbox for 'select all subscriptions' read allVisibleSubscriptionIds,
subscriptionSelection, isMultiTariff, toggleAllSubscriptions from a stale
closure because those values were declared AFTER the columns useMemo. The
warned-about deps were absent, so the header only refreshed by accident
when expandedRows or getFilteredSubs changed.

Relocate filteredUsers / allVisibleSubscriptionIds / toggleAllSubscriptions
above the columns block, list them as deps. Header now reflects current
selection without piggybacking on unrelated re-renders.

Same commit clears the matching AdminInfoPageEditor warning with an
explicit eslint-disable-line comment — the activeLocale omission was
already intentional (initial-content lock), just not silenced.
2026-05-26 13:41:22 +03:00
c0mrade
7426e1e6d1 perf(admin-panel): migrate system info + stats poll to React Query
60s setInterval becomes refetchInterval, dropping the cancelled-flag
ceremony and the manual loading/setState plumbing. Two independent
queries — one failure no longer poisons the other (Promise.all
previously rejected the whole batch on a single 5xx).
2026-05-26 13:35:39 +03:00
c0mrade
bc5e95a6e5 perf(admin-bulk-actions): migrate users + 4 lookup loaders to React Query
User list query keys all 8 filter inputs so pagination/search/filter
changes auto-refetch with proper dedup. Tariffs/promoGroups/campaigns/
partners become long-staleTime lookup queries — fired once and cached
across remounts of the page. Existing setUsers/setTariffs/etc. setters
stay; sync useEffects copy query.data into them so downstream selection
and handler code is untouched. loadUsers becomes a thin refetch wrapper
for handleRefresh and mutation handlers.
2026-05-26 13:32:43 +03:00
c0mrade
6f7bd10d61 perf(admin-user-detail): migrate remaining 9 leaf loaders to React Query
Drops manual activeTab/userId loader triggers — query enabled
gating now handles all per-tab fetching. Wrappers around refetch
keep mutation handler call sites unchanged. Removes 4 dead
wrappers (loadTariffs/Referrals/Gifts/PromoGroups) only called
by the now-deleted activeTab useEffect.
2026-05-26 13:19:28 +03:00
c0mrade
5ee97e0a9f perf(admin): migrate AdminUserDetail loadUser to React Query (caching + cache key)
Replace the 13-line manual fetch with a useQuery hook keyed on userId. loadUser is
kept as a thin wrapper around userQuery.refetch() so all 25+ existing call sites in
mutation handlers continue to work unchanged. Sync useEffects copy data → setUser,
isFetching → setLoading, and isError → navigate, preserving the original
'load fails → redirect to /admin/users' behavior.

Remaining 12 loaders (loadSyncStatus/loadTariffs/loadTickets/loadReferrals/loadPanelInfo/
loadNodeUsage/loadDevices/loadSubscriptionData/loadGifts/loadPromoGroups/loadTicketDetail/
loadRequestHistory) NOT migrated in this commit — some are parameterized, some chain via
Promise.all in loadSubscriptionData; they need a dedicated refactor with functional
testing across all 7 tabs and the mutation flows. This commit ships the lowest-risk,
highest-leverage piece (userQuery has the most call sites).
2026-05-26 13:03:04 +03:00
c0mrade
1e4d6dad47 perf(admin): migrate AdminBanSystem tab fetching to React Query
All 10 tab data fetches + the initial status load move from manual useState +
useEffect + loadTabData(switch) pattern to per-tab useQuery hooks with enabled
gating. Each query syncs into the existing state vars via useEffect so the JSX
and mutation handlers stay unchanged; handleSearch/handleUnban/handleToggle/handleSet
now call query.refetch() directly; refresh button uses refetchActiveTab helper.

Net effect: tab switches return to cached data instantly (background revalidate),
no duplicate fetches, no manual loading/error wiring. The reports query has
reportHours in its queryKey so changing the period auto-refetches without the
extra useEffect that previously handled it.
2026-05-26 12:55:28 +03:00
c0mrade
7e9e51ba5a fix(a11y): announce Login form-level error via role=alert 2026-05-26 12:09:57 +03:00
c0mrade
92ec9c3c42 fix(a11y): announce form-level errors via role=alert in Login + ResetPassword
Wrap the error message blocks in role=alert so assistive technology announces
authentication failures and reset-password errors when they appear (WCAG 4.1.3).
2026-05-26 12:08:43 +03:00
c0mrade
9f06526238 fix(a11y): aria-label on icon-only buttons (Subscription copy + InstallationGuide back/QR)
Subscription copy-URL button only had a title attribute; back and QR-open buttons in
InstallationGuide had no accessible name. Add aria-label so screen readers announce
the action (WCAG 2.4.6).
2026-05-26 12:06:15 +03:00
c0mrade
0b07af82d4 perf(admin): migrate AdminDashboard to React Query (refetchInterval, no setInterval)
Replace manual fetchStats/fetchExtendedStats (useState + useEffect + setInterval(30s)
+ console.error) with two useQuery hooks using refetchInterval. Keeps existing variable
names (stats/loading/error/referrers/campaigns/payments/systemInfo) so JSX is unchanged.
handleRestartNode/handleToggleNode/retry button now refetch via the query.
2026-05-26 11:52:14 +03:00
c0mrade
dadf08d005 perf(admin): migrate AdminUsers fetch to React Query
Replace manual useState + useEffect + useCallback + console.error fetch pattern with
useQuery for the users list and stats. Adds caching, dedupe, stale-time, automatic
loading/error state — and the refresh button now uses refetch() (single source of truth).
2026-05-26 11:36:02 +03:00
c0mrade
27ba50747a fix(a11y): radiogroup semantics for Campaign bonus-type selector
AdminCampaignCreate + AdminCampaignEdit: bonus-type button grid now uses
role=radiogroup + aria-labelledby + role=radio + aria-checked (WCAG 4.1.2).
2026-05-26 11:32:32 +03:00
c0mrade
30c4c2ecc6 fix(a11y): radiogroup semantics for AdminInfoPageEditor button groups
Wrap page-type and locale button groups in role=radiogroup + aria-labelledby; mark
each option as role=radio with aria-checked (WCAG 4.1.2).
2026-05-26 11:30:18 +03:00
c0mrade
581a2ffb52 fix(a11y): radiogroup semantics for AdminPromoOfferSend button groups
Wrap template-selection and send-mode button groups in role=radiogroup +
aria-labelledby; mark each option button as role=radio with aria-checked
(WCAG 4.1.2). Labels get matching id for the labelledby relationship.
2026-05-26 11:28:37 +03:00
c0mrade
9bb1c2339a refactor(theming): blue-* → accent-* in admin/internal usages
Sweep non-brand blue across admin pages, AnalyticsTab, TvQuickConnect, and menuLayout
style picker to accent tokens. Telegram-brand blue kept in BroadcastPreview (TG chat
mockup), the blocking screens (TG channel/bot CTAs), and the SuccessNotificationModal hero.
2026-05-26 11:24:28 +03:00
c0mrade
3df2134cba refactor(theming): orange-* → warning-* for promo/highlight semantics
Sweep orange across SubscriptionPurchase/GiftSubscription/PromoOffersSection/
AdminTrafficUsage/AnalyticsTab/withdrawalUtils to warning-* tokens. Stars brand
yellow→orange gradient kept in TopUpAmount.
2026-05-26 11:22:19 +03:00
c0mrade
d7cbbe077a refactor(theming): yellow-* → warning-* for status/pending semantics
Sweep yellow color classes that signal pending/warning status (admin pending pills,
withdrawal status, risk indicators) → warning-* tokens. Telegram Stars / fortune wheel
yellow kept as brand (TopUpAmount + Wheel + FortuneWheel).
2026-05-26 11:21:16 +03:00
c0mrade
122f050a1b refactor(design): flatten accent gradient CTAs to solid bg per design law
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.
2026-05-26 11:17:42 +03:00
c0mrade
feee9f9c05 fix(theming): replace off-token TelegramCallback/VerifyEmail with design tokens
Both pages were rendering with raw Tailwind gray-* + a non-existent border-primary-600
class (invisible spinner). Map: bg-gray-50 → bg-dark-950, text-gray-900 → text-dark-50,
text-gray-500 → text-dark-400, border-primary-600 → border-accent-500. Pages now match
the dark theme and the spinner is visible.
2026-05-26 11:14:58 +03:00
c0mrade
bd8a4fad6e refactor(theming): replace amber-* with warning-* tokens
Sweep all amber-N color classes to warning-N (uniformly pending/warning semantics).
Yellow and orange left for per-file review (mixed Stars brand + semantic uses).
2026-05-26 11:10:50 +03:00
c0mrade
cb7693e3ff refactor(theming): replace green-* / emerald-* with success-* tokens
Sweep: text/bg/border/ring/from/to/via/fill/stroke/shadow-{green,emerald}-N → success-N
(all were semantic success/active/paid states).
2026-05-26 11:09:28 +03:00
c0mrade
9c5e38b594 refactor(theming): replace red-* palette with semantic error-* tokens
Sweep 30 files: text/bg/border/ring/from/to/via/fill/stroke/shadow/divide/decoration/
outline/placeholder-red-N → -error-N. All red usages were semantically error/danger
(no brand red), so tokens now flow through the design-system CSS variables and respond
to palette overrides.
2026-05-26 11:08:22 +03:00
c0mrade
e1b48c1ccb fix(a11y): label associations for AdminTariffCreate fields
Add htmlFor/id pairs for name, description, daily price, traffic limit, device limit
and tier level (WCAG 1.3.1).
2026-05-26 10:48:46 +03:00
c0mrade
1ec6d7e979 fix(a11y): label associations for AdminPromoOfferTemplateEdit fields
Add htmlFor/id pairs for template name, message text, button text, valid hours,
discount percent, test duration, and active discount hours (WCAG 1.3.1). Test-squads
section is a checkbox group, not a single control.
2026-05-26 10:45:29 +03:00
c0mrade
d42fcfa5dd fix(a11y): label associations for AdminServerEdit fields
Add htmlFor/id pairs for display name, description, country code, price, max users
and sort order (WCAG 1.3.1). Original name is a read-only display, not a control.
2026-05-26 10:42:15 +03:00
c0mrade
5a8195f83f fix(a11y): label associations for AdminPromoGroupCreate + AdminPinnedMessageCreate
Add htmlFor/id pairs for the name + auto-assign inputs (AdminPromoGroupCreate) and the
content textarea (AdminPinnedMessageCreate). AdminPromoOfferSend skipped — its labels
sit above button-groups, not native controls (a separate role=group/radiogroup pass).
2026-05-26 10:39:22 +03:00
c0mrade
a635ca2316 fix(a11y): label associations for AdminPromocodeCreate fields
Add htmlFor/id pairs for code, type, balance bonus, days count, tariff, promo group,
discount percent, discount validity, max uses and valid-until inputs (WCAG 1.3.1).
2026-05-26 10:37:01 +03:00
c0mrade
6f66cb6759 fix(a11y): label associations for AdminCampaignEdit fields
Add htmlFor/id pairs for tariff selector, partner selector, name, start parameter,
subscription days/traffic/devices and tariff duration days inputs (WCAG 1.3.1).
2026-05-26 10:24:58 +03:00
c0mrade
4d18be848d fix(a11y): label associations for AdminCampaignCreate fields
Add htmlFor/id pairs for tariff selector, name, start parameter, trial subscription
days/traffic/devices and tariff duration days inputs (WCAG 1.3.1). Group labels (server
selector, bonus-type buttons) deferred — they need role=group, not htmlFor.
2026-05-26 10:20:56 +03:00
c0mrade
a4897243ba fix(a11y): label associations for AdminInfoPageEditor fields
Add htmlFor/id pairs for slug, icon, sort-order, replaces-tab and title inputs (WCAG 1.3.1).
2026-05-26 00:40:12 +03:00
c0mrade
fefc52fb20 fix(a11y): label associations for AdminNewsCreate fields
- htmlFor/id pairs for title, slug, read-time, excerpt and featured-image inputs
- add an ariaLabel prop to ColoredItemCombobox (applied to the trigger button) and
  pass the category/tag labels so the comboboxes have an accessible name (WCAG 1.3.1/4.1.2)
2026-05-26 00:20:30 +03:00
c0mrade
1b40c3856d fix(a11y): associate referral form labels with their inputs
Add htmlFor/id pairs across the partner-apply form (company, channel, website,
description, expected referrals, desired commission) and the withdrawal-request form
(amount, payment details) so labels are programmatically tied to their controls (WCAG 1.3.1).
2026-05-26 00:06:59 +03:00
c0mrade
7b0a72db5b fix(a11y): switch semantics for admin form toggles
Add role=switch + aria-checked + aria-label to 12 ad-hoc toggle buttons across admin
create/edit forms (AdminCampaignCreate/Edit, AdminPromocodeCreate, AdminPromoGroupCreate,
AdminPromoOfferTemplateEdit, AdminPaymentMethodEdit, AdminTariffCreate) so screen readers
announce on/off state (WCAG 4.1.2).
2026-05-26 00:03:17 +03:00
c0mrade
491f48a331 fix(a11y): switch semantics for user-flow toggles
Add role=switch + aria-checked + aria-label to the custom toggle buttons so screen
readers announce on/off state (WCAG 4.1.2): autopay (Subscription) and the custom-days
/ custom-traffic toggles (SubscriptionPurchase).
2026-05-25 23:57:36 +03:00
c0mrade
90888db48c fix(a11y): associate Support ticket form labels with their inputs
Add htmlFor/id pairs for the create-ticket subject and message fields so screen
readers announce the label and clicking the label focuses the control (WCAG 1.3.1).
2026-05-25 23:53:42 +03:00
c0mrade
0caba3dffb feat(a11y,i18n): more modal focus-traps + Telegram theme/language sync
Modal a11y:
- focus-trap + role=dialog/aria-modal/aria-labelledby on AdminRoles delete confirm
  and AdminBanSystem user-detail modal (+ close aria-label)

Telegram theme/language sync (first run only, explicit choice always wins):
- getTelegramColorScheme() / getTelegramLanguageCode() helpers in useTelegramSDK
- useTheme initial state follows the Telegram client color scheme when no stored theme
- applyTelegramLanguage() adopts the Telegram client language when no stored choice,
  called from main.tsx after SDK init (launch params unavailable before init)
2026-05-25 23:31:38 +03:00
c0mrade
8e0b63bac8 feat(a11y): cross-platform hardening + modal focus-trap from impeccable audit
Responsive / viewport:
- add .min-h-viewport / .h-viewport utilities (100dvh + Telegram --tg-viewport-stable-height)
- migrate min-h-screen / h-screen / 100vh across 18 files
- reveal hover-only controls on focus-within + touch (desktop nav, admin settings)
- grid breakpoints (TopUpAmount, Contests), larger touch targets, nav aria-labels

Platform routing (Telegram WebView correctness):
- clipboard -> copyToClipboard util with execCommand fallback (10 files)
- window.open -> openTelegramLink / openLink (Profile, Referral, ChannelSubscriptionScreen)
- window.confirm -> useNativeDialog (admin pages); PromoOffersSection -> useDestructiveConfirm
- window.prompt -> usePrompt / PromptDialogHost (TipTap link editors)
- ESLint no-restricted-properties guard against regressions (adapters/clipboard util exempt)

Modal accessibility:
- new useFocusTrap hook (focus trap, Esc, scroll lock, focus restore)
- role=dialog / aria-modal / focus-trap: Polls, SuccessNotificationModal, AdminPolicies,
  AdminPromoGroups, AdminCampaigns, BroadcastPreview (Telegram + Email)
- new global usePrompt store + PromptDialogHost
2026-05-25 23:16:07 +03:00
Fringg
e44a09312e fix(subscription): forward subscription_id to purchaseTariff to fix renew race
Companion to bedolaga-bot a527df23. Bot's purchase-tariff endpoint
now accepts optional subscription_id to resolve the EXACT target row
by ID instead of doing a (user_id, tariff_id) re-lookup that races
with concurrent panel webhooks (produces 'Тариф уже активен' +
refund + no extension).

Frontend changes:
* subscriptionApi.purchaseTariff() — new optional subscriptionId arg
  appended after trafficGb. Forwarded as subscription_id in POST body.
* SubscriptionPurchase.tsx — forwards URL searchParam subscriptionId
  (set when user lands here from 'Renew this subscription').
* SubscriptionCardExpired.tsx — passes subscription.id when renewing
  an expired daily tariff (the .purchaseTariff(tariff_id, 1) call).
2026-05-16 16:10:32 +03:00
Fringg
47f0359ed6 fix(subscription): invalidate devices query after revoke
On revoke, RemnaWave panel clears all device HWIDs along with the
subscription URL rotation, but the cabinet was only invalidating
['subscription'], ['connection-link'], ['subscriptions-list'] — leaving
['devices', subscriptionId] cached. UI showed the now-stale device list
until the user manually refreshed.

The GET /subscription/devices endpoint reads HWIDs live from RemnaWave
(no DB cache to clear), so invalidating the TanStack query is
sufficient — the immediate refetch returns the now-empty device list.
2026-05-16 04:34:32 +03:00
Fringg
e964780e6f refactor(devices): close all MED/LOW review items
- Extract DEVICE_ALIAS_MAX_LENGTH to src/constants/devices.ts so the
  user page and admin page share one source of truth (mirroring the
  bot's ALIAS_MAX_LENGTH=64). Was duplicated as a magic 64 in both
  files (MED-5).
- Replace ✓ / ✕ / ✏️ glyphs in AdminUserDetail.tsx with inline SVGs,
  matching the icon style already used by Subscription.tsx for the
  same rename/save/cancel actions. Eliminates the platform-dependent
  emoji rendering inconsistency (MED-4).
- Add haptic feedback to the user-side renameDeviceMutation —
  notification('success') on save, notification('error') on failure.
  Brings it in line with the other Subscription.tsx mutations
  (LOW-9).
- Translate the new rename keys into zh.json and fa.json (en.json
  already covered in the previous follow-up). Cabinet now ships full
  parity across all 4 locales for the rename UI.
2026-05-16 04:07:25 +03:00
Fringg
d1e5ce873b fix(devices): unicode escape bug + onSuccess race + en locale + apiErr surface
Code-review follow-up on 321c65b.

- prettier ate the raw glyphs (✓/✕/✏️) in AdminUserDetail.tsx and emitted
  \u2713 / \u2715 / \u270F\uFE0F as JSX text children. JSX does NOT
  process \u… escapes inside element children, so admin users saw the
  literal six-character strings on Save/Cancel/Rename buttons. Wrap each
  glyph in a JS expression {'…'} so it survives both prettier and JSX.
- renameDeviceMutation.onSuccess on Subscription.tsx no longer
  unconditionally clears the edit state. If the user already navigated
  to a different device while the previous save was in flight, we keep
  their in-progress input. Same shape applied to AdminUserDetail.tsx —
  the alias is snapshot before the await so a race can't smuggle the
  wrong value into the request, and the post-success reset is guarded.
- AdminUserDetail rename error path now surfaces the backend's
  response.data.detail (e.g. '64-char limit' message) instead of the
  generic localized userActions.error toast.
- Add the new rename i18n keys to src/locales/en.json so English users
  no longer fall back to Russian labels. ua/zh/fa to follow in a
  dedicated translation pass.
2026-05-16 03:53:49 +03:00
Fringg
321c65b68b feat(devices): inline rename UI for connected HWID devices
Pairs with the bot commit that adds the user_device_aliases table.
Surfaces the new local_name field on the existing devices list in
both the user-facing subscription page and the admin user-detail
page, with the same inline-edit pattern in both places.

User-side (src/pages/Subscription.tsx):
- pencil button next to each device row toggles edit mode
- input is focused automatically, capped at 64 chars (matches the
  backend ALIAS_MAX_LENGTH and DB column width)
- Enter saves, Escape cancels, empty input clears the alias
- display priority: local_name → device_model → platform
- works identically in classic / single-tariff / multi-tariff —
  subscriptionId is forwarded as a query param like every other
  device-management endpoint already does

Admin-side (src/pages/AdminUserDetail.tsx):
- same pencil + inline input pattern, admin acts on behalf of the
  user. notify.success on save, loadDevices() refresh.

API (src/api/subscription.ts + src/api/adminUsers.ts):
- new renameDevice(hwid, name, subscriptionId?) on subscriptionApi
- new renameUserDevice(userId, hwid, name) on adminUsersApi
- existing getDevices/getUserDevices contracts widened with
  local_name?: string | null on the returned device shape

Locales (src/locales/ru.json):
- subscription.renameDevice / .renameDeviceSave / .renameDeviceCancel
  / .renameDevicePlaceholder / .deviceRenamed
- admin.users.detail.devices.rename / .renameSave / .renamed
2026-05-16 03:02:45 +03:00
Fringg
f301d44f24 fix(admin): show flag for every country code, not just hardcoded 25-36
User reported the EE (Estonia) flag rendered as plain text 'EE' on the
admin servers page. Root cause: getCountryFlag() in AdminServers.tsx
and AdminServerEdit.tsx hardcoded a 25-entry codeMap that didn't
include EE; falling through to 'return code' produced raw text.

Two other admin pages (AdminRemnawaveSquadDetail, AdminUserDetail)
had bigger 35-entry maps that did include EE — so EE worked there
but MX/AR/EG/ZA and friends still wouldn't. AdminRemnawave repeated
the same 35-entry map. AdminTrafficUsage already had the correct
algorithmic ISO→regional-indicator code but as a local duplicate of
utils/subscriptionHelpers.

Unify all six on the single algorithmic helper:

- getFlagEmoji() in utils/subscriptionHelpers.ts now:
  * accepts string | null | undefined (callers don't need to guard)
  * trims whitespace
  * validates [A-Za-z]{2} before composing regional indicators
- Each admin page now either imports getFlagEmoji directly or wraps
  it with the page's preferred fallback character (e.g. '🌍' for
  empty codes where the UI expects a placeholder).
2026-05-16 01:11:00 +03:00
Fringg
abbbc6a216 fix(onboarding): prevent invisible overlay blocking clicks and stuck tour
- Tie spotlight/tooltip pointer-events to isVisible so opacity:0 stops
  trapping taps during step transitions
- Render target click-catcher only when overlay is fully visible and
  clear stale targetRect on step change
- Retry target lookup up to 6 times and auto-skip step (or complete
  tour) when target is missing, instead of leaving overlay stuck
  invisible-but-interactive
- Persist completion to localStorage before unmounting the tour, so
  the flag survives even if unmount throws
2026-05-15 05:34:38 +03:00
Fringg
5493c25e5f chore(topup): harden direct-open guard and admin toggle defenses
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.
2026-05-13 11:13:33 +03:00
Fringg
aa8bfc9d08 feat(topup): direct-open payment page when method.open_url_direct is set
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.
2026-05-13 10:50:31 +03:00
Fringg
2a342f6adc fix(topup): preserve canonical RUB amount to avoid FX round-trip shortfall
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.
2026-05-13 10:24:08 +03:00
Fringg
172850e13a fix(profile): use displayName helper for name field
Follow-up to displayName migration. Profile page still rendered
`{user?.first_name} {user?.last_name}` directly, leaving a trailing space when
last_name is null. displayName() handles the joining and fallback uniformly.
2026-05-13 09:20:48 +03:00