Commit Graph

430 Commits

Author SHA1 Message Date
c0mrade
5e221d471e refactor(subscription): extract DeviceTopupSheet from god page
Move the buy-devices sheet (190 lines of inline JSX + its useQuery +
useMutation) into src/components/subscription/sheets/DeviceTopupSheet.tsx.
Parent passes shared state (purchaseOptions, devicesToAdd, ids); the
sheet self-owns the device-price query and purchase mutation, so the
query only fires when the sheet is actually open.

Subscription.tsx: 2671 → 2477 lines (−194).
2026-05-26 22:12:03 +03:00
c0mrade
b83dced3af distill(SubscriptionCardActive): strip 3 decorative-only chromes
Critique flagged the card as carrying '12 chrome elements for 2 facts'.
Removing 8 of 12 without browser QA risks gutting functional info.
Apply the conservative 3-element strip: only kill chromes with zero
information value.

  • Background radial glow (200x200 circle, top-right, decorative tint
    based on traffic zone) — pure ornament, no signal.
  • Trial shimmer border (animate-trial-glow on inset wrapper) — trial
    state is already signalled by the badge in the header; the second
    signal was visual noise.
  • Trial badge own animate-trial-glow + 2-stop gradient bg — flatten
    to single bg-accent-400/10 + border-accent-400/25 + solid text.
    The badge still says TRIAL with brand color; it no longer pulses.
  • Zone dot 8px-radius color-tinted boxShadow + 'transition: all' —
    drop the glow, transition only background. Reduces filter+composite
    work, dot still signals zone via color.

Kept (functional):
  • Big percentage + traffic-used metadata (primary info, not chrome)
  • Progress bar (primary signal)
  • Connect Device CTA with HoverBorderGradient (primary action;
    rotating border is its own UX debate, deferred)
  • Sparkline + connected devices count (functional metadata)

Full 12→4 collapse the critique recommended needs browser QA and a
design-direction call (move days/tariff/refresh to the detail page
header). Bigger restructure as separate PR.
2026-05-26 21:56:13 +03:00
c0mrade
f75b243f03 polish: clear 33/34 detector findings (bg-black, animate-bounce, spinner, transition-width, color)
After re-audit/critique cycle the deterministic detector (npx impeccable)
flagged 34 design-spec violations. Knock out 33 of them.

  • 25 × pure-black-white: sed-sweep bg-black/X → bg-dark-950/X across
    18+ files (modal scrims, photo viewer backdrop, code blocks). The
    base resolves to rgba(10,15,26,X) — visually identical to true
    black, satisfies the 'no #000' impeccable rule.
  • 3 × bounce-easing: SuccessNotificationModal celebration icon and
    SyncTab loading arrows used animate-bounce; replaced with
    animate-pulse. Bounce easing reads dated; pulse conveys 'in
    progress' without the cartoon feel.
  • 3 × border-accent-on-rounded: TelegramCallback + VerifyEmail
    spinners used 'border-b-2 border-accent-500' on rounded-full —
    detector reads it as a side-stripe even though it's a ring loader.
    Switch to canonical 'border-2 border-accent-500 border-t-transparent'
    (3/4 ring colored). Same visual, no spec violation.
  • 1 × ai-color-palette: AdminLandingStats had text-purple-400 on a
    gift-stats heading; purple is not in the brand palette. Swap to
    text-accent-400.
  • 1 × layout-transition: TrafficProgressBar.tsx fill bar still used
    transition: width 1.2s (slipped past the earlier optimize pass).
    Convert to transform: scaleX with origin-left. Same gradient, same
    duration, runs on the compositor.

Remaining: 1 finding in third-party Aceternity background-beams-collision
component (indigo-500 gradient on decorative WebGL background) — left
as-is, it's lifted decorative third-party code.

Detector: 34 → 1.
2026-05-26 21:52:56 +03:00
c0mrade
251c862922 adapt(admin): mobile touch-target floor on shared .btn + ticket chat
Audit P2: admin pages prioritise density but cabinet also runs in the
Telegram Mini App on phones. Small button targets (~24-32px) work for
mouse precision but fail mobile thumbs. Apply targeted bumps that keep
desktop density unchanged.

src/styles/globals.css
  • .btn — add min-h-[44px] on mobile, sm:min-h-0 on desktop.
    Cascades to every .btn-primary / .btn-secondary / .btn-ghost /
    .btn-danger consumer (76 + 48 + N usages app-wide). One change,
    everywhere.
  • .btn-icon — same logic (min-h/min-w 44px mobile). Drops the
    duplicate definition that appeared further down the file.

components/admin/userDetail/TicketsTab.tsx — mobile-critical chat
  • Back button h-8 w-8 → h-10 w-10 sm:h-8 sm:w-8 (40px / 32px).
    Adds aria-label since it had none.
  • Status pills px-2.5 py-1 → +min-h-[36px] py-1.5 sm:min-h-0
    sm:py-1 (36px / 26px). Mis-tap penalty on mobile is high (it
    flips ticket status).
  • Reply send button — add min-h/min-w 44px mobile + aria-label.

Why not blanket bump py-1.5 → py-2.5 across all 40+ admin pages:
visual density patterns were iteratively designed and a global sweep
would regress them. Per-page review can target specific buttons that
deserve mobile touch floor without breaking density elsewhere.

icon-only buttons not using .btn-icon (raw <button><svg/></button>)
left as-is — they're typically in compact toolbars where visual
density is the point. Per-page review under /impeccable adapt
target=<page> if mobile flow analysis flags one.
2026-05-26 20:53:40 +03:00
c0mrade
cef675825b refactor(tokens): extract subscription urgent + critical semantic colors
The #FFB800 (23 hits) and #FF3B5C (11 hits) raw hex literals across
Subscription / SubscriptionPurchase / TrialOfferCard / useTrafficZone
all represent the same semantic: 'expiring soon' and 'expired'.
Distinct from warning-500 (#f59e0b) and error-500 (#ef4444) — the
subscription timeline needs a sharper, hotter signal than the
system-wide warning/error roles.

Add two semantic tokens:
  --color-urgent-400:   255, 184, 0    (#FFB800)
  --color-critical-500: 255, 59, 92    (#FF3B5C)

Exposed via Tailwind as urgent.400 / critical.500. Sweep all
single-and double-quoted hex literals to rgb(var(--color-...))
form so style attrs, SVG stroke=, and ternary string values all
resolve through CSS vars (auto-respects future light-theme
variants).

Left as-is: 4 hex appearances inside gradient pair strings —
linear-gradient(135deg, #FFB800, #FF8C00) — where the partner
color (#FF8C00 / #FF6B35) isn't in the token set. Partial-replace
inside gradient strings would just add noise; full extraction of
the orange-ramp companion is out of scope for this pass.
2026-05-26 20:20:05 +03:00
c0mrade
eaaf5cfebb fix(toast): drop side-stripe ban + a11y polish
Per /impeccable audit: Toast.tsx had a 4px border-l accent on top of
a 1px full border — the explicit 'side-stripe' absolute ban. Toast is
on screen after every notify.* call, so the violation repeated
constantly.

Polish pass:
  • Replace 'border border-l-4 border-dark-700 ${style.border}' with
    a single tinted full border (border-success-500/40, etc). Semantic
    still carries through the colored icon box + matching border tint.
  • Drop bg-tint flood — the icon box at 40px is enough at this scale.
  • Reduce shadow weight (shadow-2xl shadow-black/50 → shadow-xl
    shadow-black/30). Floating layer needs elevation, not drama.
  • Soften interaction scale (1.02 hover → 1.01, 0.98 active → 0.99)
    to match a calmer overall feel.
  • Thinner progress bar (h-1 → h-0.5) — the countdown is ambient, not
    primary content.

A11y:
  • Container: role='region' + aria-label + aria-live='polite' so SR
    announces incoming toasts without stealing focus.
  • Toast: role='alert' for type=error (interrupts SR — appropriate
    for failures), role='status' for everything else.
  • Toast: tabIndex=0 + Enter/Space to activate, Escape to dismiss —
    was click-only before, useless on keyboard.
  • focus-visible: 2px ring at accent-500/60 with offset for visibility
    on the dark backdrop.
  • Icon box aria-hidden — it's decorative reinforcement, the role +
    text content is what SR reads.
  • Progress bar aria-hidden — visual countdown only.

shrink keyframe already used transform: scaleX (no layout thrash)
and slide-in-right keyframe already used ease-out-expo — both
align with motion laws as-is, kept unchanged.
2026-05-26 19:17:39 +03:00
c0mrade
9afa7f131f refactor(admin-user-detail): extract Subscription tab — page under 1000 lines
Last and biggest tab — extracted via view-facade pattern (like Info).
JSX moves into components/admin/userDetail/SubscriptionTab.tsx;
state, queries, mutation handlers stay in parent because most are
shared with other tabs (panelInfo + devicesQuery used by Info,
inline-confirm armer used by destructive actions, etc.).

The new file is 1223 lines — over the soft cap, but cohesive: one
tab with multi-sub list, sub-detail view, traffic packages,
devices CRUD with inline rename, panel info/config, live traffic,
node usage, and request history table. Sub-extracting (Devices /
RequestHistory / PanelLinks) is the next step but not free —
each adds prop-passing boilerplate.

Wide prop interface (~50 props) but every prop is a single pass-
through line at the call site. No state moves; just JSX reroute.

Parent imports trimmed: DEVICE_ALIAS_MAX_LENGTH, getFlagEmoji,
createNumberInputHandler, getCountryFlag, PlusIcon, MinusIcon,
StatusBadge — all gone, subscription tab was their last consumer.
RefreshIcon kept (page header reload button still uses it).

Net to AdminUserDetail.tsx: down to 970 lines (-1022). Total
across the 7 tab extractions: 3820 → 970 (-2850, -75%).
2026-05-26 18:39:08 +03:00
c0mrade
f6f3c21768 refactor(admin-user-detail): extract Info tab as view facade
The info tab needed a different shape than the prior 5 — its
panelInfo, promoGroups, editingPromoGroup, action handlers
(reset-trial, disable, full-delete) are SHARED with the subscription
tab and with the inline-confirm armer up in the parent. Moving any
of that into the tab would either duplicate the query/state or
fork the confirm state. So the new InfoTab is a 'view facade':
state and handlers stay parent-owned, the JSX moves out.

Prop surface is wide (~25 props) but every prop is a single
'pass through' line at the call site — no logic shifted. Each
handler / state slot still lives in exactly one place; the tab
only renders.

Bonus: the parent's formatWithCurrency / useCurrency import is
dropped — info tab was the last consumer in the page body.

Net to AdminUserDetail.tsx: down to 1992 lines (-403). Total
journey across the 6 tab extractions: 3820 → 1992 (-1828, -48%).
Only the subscription tab JSX (~1043 lines) remains inline.
2026-05-26 18:17:27 +03:00
c0mrade
fab2a504d3 refactor(admin-user-detail): extract Tickets tab (list + chat view)
Pulls the tickets tab into components/admin/userDetail/TicketsTab.tsx:
  - ticketsQuery (list)
  - selectedTicketId / selectedTicket state + detail loader
  - reply form state (replyText, replySending)
  - status-change action handler + actionLoading
  - auto-scroll messagesEndRef
  - JSX is split into ChatView + TicketsList + EmptyState helpers
    inside the new file (file stays under 400 lines despite the
    split-view UI)

Parent now passes only userId + formatDate. Imports for adminApi,
MessageMediaGrid, linkifyText, AdminTicket / AdminTicketDetail types
all drop from AdminUserDetail.tsx — tickets are the only consumers.

Net to AdminUserDetail.tsx: down to 2395 lines (-295). Same DOM,
same event handlers, same queryKey ['admin-user-tickets', userId].
2026-05-26 18:00:02 +03:00
c0mrade
24c57cf985 refactor(admin-user-detail): extract Balance tab with own state + mutations
Pulls the balance tab — current balance, add/subtract form, active
promo offer summary + deactivate, send-offer form, recent
transactions list — into components/admin/userDetail/BalanceTab.tsx.
Form state (balanceAmount, balanceDescription, offerDiscountPercent,
offerValidHours), mutation handlers (handleUpdateBalance,
handleDeactivateOffer, handleSendOffer), and the inline two-click
confirm armer all live local now.

Tab takes a small prop surface — user, userId, hasPermission,
onUserRefresh, formatDate — and owns its own actionLoading /
offerSending flags so the deactivate-offer confirm doesn't dim
buttons on other tabs.

Net to AdminUserDetail.tsx: down to 2690 lines (-212). The
promoOffersApi import is dropped from the parent — it's only the
balance tab that talks to that endpoint now. PlusIcon / MinusIcon
are still used by the subscription tab in the parent, so they stay
there too.
2026-05-26 17:51:24 +03:00
c0mrade
18e030c9b9 refactor(admin-user-detail): extract Referrals tab with own state + query
Pulls the entire referrals tab into components/admin/userDetail/
ReferralsTab.tsx — state, useQuery, debounced search effects,
click-outside listeners, mutation handlers, and the 295-line JSX
block all live there now. The tab owns its referralsListQuery and
calls onUserRefresh() (parent's loadUser) when a mutation needs to
re-fetch the user object.

Net to AdminUserDetail.tsx: down to 2902 lines (-479). Local
'actionLoading' inside the tab means referral-mutation clicks no
longer dim balance/sync buttons. Same DOM, same handlers, same
queryKey ['admin-user-referrals-list', userId].
2026-05-26 17:43:58 +03:00
c0mrade
b9acc822f2 refactor(admin-user-detail): extract Gifts + Sync tabs to own components
AdminUserDetail.tsx was 3820 lines — a single page bigger than the
project's 800-line soft cap by 4.7x. Split the two most self-contained
tabs (no mutations / setState from within the tab body) into their
own files under src/components/admin/userDetail/:

  GiftsTab.tsx (299 lines)
    - GiftStatusBadge       (badge for one of 6 lifecycle states)
    - GiftCard              (single row, sent or received)
    - GiftsTab              (sent + received sections + counters)
    props: giftsLoading, giftsData, locale, onNavigateToUser

  SyncTab.tsx (207 lines)
    - ArrowDownIcon, ArrowUpIcon (used only by sync action buttons)
    - SyncTab (bot vs panel comparison + 2-way sync)
    props: user, syncStatus, userSubscriptions, activeSubscriptionId,
           onActiveSubscriptionChange, actionLoading, onSyncFromPanel,
           onSyncToPanel, locale

Net: AdminUserDetail.tsx down to 3381 lines (-439). No behaviour change:
the tabs render the exact same DOM, same SVG paths, same event handlers
just routed through props.
2026-05-26 17:12:47 +03:00
c0mrade
b6613ae4c9 perf(images): add loading=lazy to in-list previews and attachments
Four image sites that render thumbnails/previews inside lists or tab
panels now hint loading=lazy so the browser defers the network fetch
until the element is near the viewport:
- Support.tsx attachment preview grid
- AdminTickets.tsx reply attachment grid
- BroadcastPreview.tsx photo preview
- BrandingTab.tsx logo preview in admin settings

Top-of-page logos (AppHeader/AppShell/DesktopSidebar/Login/Telegram
redirect/DeepLink) intentionally stay eager — they're above the fold
and should paint on first frame. Payment-method icons (6x6 px) are
already negligible. Compose attachment previews use blob: URLs where
loading=lazy is a no-op.
2026-05-26 16:05:52 +03:00
c0mrade
cf469a1a9f chore(types): replace deprecated FormEvent with SyntheticEvent
React 19's @types/react flags FormEvent as deprecated — quote: 'FormEvent
doesn't actually exist. You probably meant to use ChangeEvent, InputEvent,
SubmitEvent, or just SyntheticEvent'. All 17 call sites in this repo
typed form onSubmit handlers and only called e.preventDefault(), so
SyntheticEvent is the correct general replacement.

No behavior change — pure type-level cleanup that clears the chronic
deprecation hint that has been showing up after every edit to any
form-bearing page.
2026-05-26 15:24:01 +03:00
c0mrade
62eaef2f53 fix(permission-route): guard against /admin → /admin redirect loop
PermissionRoute unconditionally sent permission-failing users to /admin.
The current router wraps /admin in AdminRoute (no permission check), so
this never triggered. But if /admin ever picks up a PermissionRoute
guard — even by accident in a future change — the user would loop on
itself with replace history (no back-button escape).

Detect that case explicitly: if we're already on /admin and still failing
the permission check, fall back to / instead. Costs nothing today,
prevents a hard-to-diagnose lock-out later.
2026-05-26 15:18:21 +03:00
c0mrade
28c682500c fix(i18n,a11y): sync <html lang> + dir with i18n language
index.html ships <html lang='ru'> and that attribute never changed at
runtime, regardless of the user's selected language. Consequences:
- screen readers pronounced en/zh/fa content using Russian phonics
- browsers offered to translate already-translated content
- the Telegram login button and an OAuth widget read documentElement.lang
  to set their own locale and got the wrong one

LanguageSwitcher had a half-fix that only touched dir for fa, and only
locally — i18n changes from anywhere else (initial detect, Telegram
adopt) were not covered.

Move both lang and dir updates into a single languageChanged subscriber
inside i18n.ts, run once for the initial detected language, and drop
the now-redundant dir-setting in LanguageSwitcher. Layout direction
flips automatically for the full RTL set (fa + future ar/he/ur).
2026-05-26 15:15:26 +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
8d6baf8ad0 refactor(theming): TelegramLoginButton muted text → dark-400 token 2026-05-26 11:26:47 +03:00
c0mrade
a2efb7aa48 refactor(theming): blue → accent in PageLoader + ButtonsTab style picker 2026-05-26 11:25:27 +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
ae7da0c86a refactor(theming): replace gray-* with dark-* tokens on blocking screens
Map text/bg/border-gray-N → dark-N across the four blocking screens (Maintenance,
Blacklisted, ChannelSubscription, AccountDeleted) so muted text + decorative surfaces
follow the design tokens. BroadcastPreview's email mockup and MessageMediaGrid's
fullscreen overlay intentionally keep their literal gray/white (mock chrome).
2026-05-26 11:19:49 +03:00
c0mrade
8a442e8aaa refactor(design): flatten remaining success-gradient CTA in SuccessNotificationModal 2026-05-26 11:18:48 +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
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
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
5c7d4b4888 feat(a11y): dialog semantics + focus trap for the onboarding tour
- role=dialog/aria-modal/aria-labelledby/aria-describedby on the onboarding tooltip
- trap focus inside the tooltip while the tour runs; Esc skips (lockScroll off so
  scrollIntoView can still bring each step's target into view)
- merge the focus-trap ref with the existing measurement ref via a callback ref
- aria-hidden on the click-to-advance target overlay (redundant with the Next button)
2026-05-25 23:51:26 +03:00
c0mrade
f31160208a feat(auth,a11y): Telegram CloudStorage token recovery + blocking-screen a11y
Telegram CloudStorage token recovery (resilience against WebView localStorage wipes):
- mirror the refresh token to per-user CloudStorage on login, remove it on logout
- restoreRefreshTokenFromCloud() recovers the refresh token in initialize() when
  localStorage is empty, then the normal refresh flow re-establishes the session
- move the initialize() bootstrap call from auth.ts module-load into main.tsx after
  the Telegram SDK init(), since launch params + CloudStorage are unavailable before init
- best-effort: no-ops outside Telegram and on any error -> falls back to prior behavior

Accessibility:
- role=alertdialog + aria-modal + aria-labelledby + focus management (useFocusTrap)
  on Maintenance, Blacklisted, ChannelSubscription and AccountDeleted blocking screens
- aria-label on ColorPicker Hue/Saturation/Lightness range inputs
2026-05-25 23:43:17 +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
8c336d16c7 fix(blocking): open bot deep-link via platform adapter, not window.open
Inside the Telegram WebView, window.open is intercepted by the
client and the new-tab fallback is blocked on most platforms
(Android, iOS). Use usePlatform().openTelegramLink(url) — same
pattern as the rest of the codebase (Support.tsx, etc.). The
TelegramAdapter dispatches to the WebApp SDK's openTelegramLink in
TG and falls back to window.open in the standalone web build, so
both runtimes work.
2026-05-16 05:25:41 +03:00
Fringg
16b47119c9 feat(blocking): account-deleted recovery screen with bot deep-link
Pairs with backend feat(deleted-users) — when the cabinet returns
403 {detail: {code: 'account_deleted', message, bot_username,
telegram_deep_link}} the SPA shows a dedicated screen instead of the
generic auth-error toast.

* Adds AccountDeletedScreen with localized title/description, an
  Open-bot button using the backend-provided telegram_deep_link, and a
  Retry button that clears the block + reloads (so the next request
  observes the now-revived row).
* useBlockingStore gains accountDeleted slice mirroring the existing
  maintenance/channel-subscription/blacklisted slots — consistent with
  the precedent for cross-cutting 403 codes.
* apiClient: AccountDeletedError type + isAccountDeletedError guard +
  response interceptor routes the 403 into the blocking store.
* i18n: ru/en/zh/fa parity for title, description, openBot, retry, hint.

No behavior change for ACTIVE users. BLOCKED users still get the
generic 403 (the backend keeps the message opaque for admin actions).
2026-05-16 05:16:24 +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
4de01ccd1d fix(dashboard): show full name on welcome, fix gender mismatch on trial-expired card
User reported single-letter welcome "Добро пожаловать, О!" (short first_name
"Олег"-style users) and a confused "Истекла" feminine label on the masculine
"Пробный период истёк" trial-expired card.

* Add `src/utils/displayName.ts` helper that composes `first_name + last_name`
  with `username` and `#telegram_id` fallbacks. Single source of truth for
  user-facing name rendering across the app.
* Apply `displayName(user)` in Dashboard welcome, AppHeader mobile drawer,
  and DesktopSidebar profile chip. Now a user with `first_name="О"` and
  `last_name="Иванов"` sees "О Иванов" instead of just "О".
* `SubscriptionCardExpired` — context-aware Russian label: for trial
  subscriptions render masculine "Истёк" (agrees with "пробный период"),
  for paid subscriptions keep feminine "Истекла" (agrees with "подписка").
  Uses i18next `context: subscription.is_trial ? 'trial' : ''` — falls back
  to base key for EN/ZH/FA which are grammatically neutral.
* Add `expiredDate_trial: "Истёк"` only to `ru.json` (no changes needed for
  other locales — i18next context falls back to `expiredDate`).
2026-05-13 09:02:10 +03:00
Fringg
b4eb0fa859 feat: add Lava payment provider support
- METHOD_LABELS entry for lava
- Custom SVG icon: dark base with orange-red lava flow and ember dots
- Admin settings tree node: payments_lava
- Locale strings (ru/en/zh/fa) for descriptions and admin labels
2026-05-04 20:16:01 +03:00
Fringg
7def84718b feat: add Jupiter and Donut payment provider support
- METHOD_LABELS entries for jupiter and donut
- Custom SVG icons: Jupiter (planet with ring), Donut (glazed donut)
- Admin settings tree nodes: payments_jupiter, payments_donut
- Locale strings (ru/en/zh/fa) for descriptions and admin labels
2026-05-04 19:36:35 +03:00
Fringg
7d940091fa feat: add Antilopay payment provider support
- Add antilopay to METHOD_LABELS for stats charts
- Add PaymentMethodIcon case with amber gradient SVG
- Add payments_antilopay to admin settings tree
- Add i18n keys in all 4 locales (en, ru, zh, fa)
2026-05-04 07:44:52 +03:00
Fringg
85a34b1947 feat: add Etoplatezhi payment provider support
- Add etoplatezhi to METHOD_LABELS for stats charts
- Add PaymentMethodIcon case with green gradient SVG
- Add payments_etoplatezhi to admin settings tree
- Add i18n keys in all 4 locales (en, ru, zh, fa)
2026-05-04 07:18:14 +03:00
Fringg
65f94931c5 feat: add TV Quick Connect to connection page for Android TV / Apple TV
When user selects Android TV or Apple TV platform (from non-TV device),
shows 5-char code input + QR scanner to send subscription to TV via
Happ TV API. Hybrid QR strategy: native Telegram scanner on mobile,
html5-qrcode CDN fallback on desktop. i18n keys for ru/en/zh/fa.

Based on PR #415 by @smediainfo.
2026-05-04 06:22:29 +03:00
Fringg
d21c6637bf feat(admin/broadcasts): add preview buttons for Telegram + Email broadcasts
Add BroadcastPreview component with local rendering of Telegram messages
(HTML tokenizer with tag whitelist, no dangerouslySetInnerHTML) and Email
preview (sandboxed iframe). Preview buttons appear in broadcast create page
section headers, disabled when content is empty.

Based on PR #416 by @smediainfo.
2026-05-04 06:18:12 +03:00
Fringg
58887138fc feat: add Apple IAP (apple_iap) payment method support
Add apple_iap across all payment surfaces: method label, icon (Apple
logo SVG), admin settings tree (APPLE_IAP category), and all four
locale files (en, ru, fa, zh) with payment description + tree label.
2026-05-04 06:01:20 +03:00
Fringg
80059681da feat: Yandex Metrika CID tracking, offline conversions UI, sticky pay button
- Pass yandex_cid to all auth endpoints (telegram, email, OIDC, OAuth)
- Add OfflineConvGoal interface and offline_conv_* fields to AnalyticsCounters
- Add storeYandexCid API method for server-side CID persistence
- Add analytics fields to LandingConfig (view/click goals, sticky_pay_button)
- Add yandex_cid/referrer/subid to PurchaseRequest
- Add offline conversions UI block in admin AnalyticsTab
- Add cacheYandexCid, syncYandexCid, fireAnalyticsEvent to analytics hook
- Create yandexCid.ts utility (localStorage get/set helpers)
- Add sticky pay button with portal on mobile in QuickPurchase
- Fire view/click analytics goals on landing pages
- Persist contact value and referrer/subid in session/localStorage
- Add i18n keys for offlineConv and apiKey in all 4 locales
2026-04-29 08:59:19 +03:00
Fringg
6d3010b621 feat: multi-media attachments, linkify URLs, shared MessageMediaGrid
- Add media_items array support to ticket messages (types, API clients)
- Create shared MessageMediaGrid component with photo grid, fullscreen
  viewer, keyboard nav, video/document rendering
- Replace per-page AdminMessageMedia/MessageMedia with MessageMediaGrid
- Add linkifyText util (DOMPurify-sanitized) for auto-linking URLs
- Support multi-file upload (up to 10) in AdminTickets and Support pages
- Remove unused ticketsApi import from AdminUserDetail
2026-04-29 08:45:15 +03:00
Fringg
9b1e26d4ec fix: switch component — replace motion with CSS transition-transform 2026-04-29 08:34:37 +03:00
Fringg
1d5ce2d4ea fix: complete is_limited status handling across all views
- SubscriptionListCard: show traffic progress bar for limited subs
  in multi-tariff list (was hidden because isActive excluded limited)
- SubscriptionListCard: amber border/background for limited status
  cards (was using default neutral style, inconsistent with rest of UI)
- Subscription page: hide delete button for limited subs in
  multi-tariff mode (limited subs still have valid days remaining)
- SubscriptionPurchase: allow tariff switch for limited subs
  (was blocked by is_active-only gate in canSwitch formula)
- AdminUserDetail: include limited status in "already purchased"
  tariff filter to prevent duplicate subscription creation
2026-04-23 04:04:11 +03:00
Fringg
f68b46699c fix: limited (traffic exhausted) subscription shows as expired
- Dashboard card: change date label from "Истекла" to "Активна до"
  for limited subscriptions (traffic exhausted but time remaining)
- Dashboard button: change text from "Трафик исчерпан" (dead-end) to
  "Докупить трафик" (actionable) for limited status
- Subscription page: CountdownTimer shows remaining time for limited
  subs instead of "Истекла" (was passing is_active=false)
- PurchaseCTAButton: don't treat limited as expired — limited subs
  still have time, should show extend CTA not "buy new subscription"
2026-04-23 03:56:07 +03:00
Fringg
996840f627 fix: change aurapay icon to pink gradient to avoid kassa_ai color collision 2026-04-18 00:12:15 +03:00