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)
Новый пользователь с доступным триалом видел только TrialOfferCard (CTA
активации), без пути в витрину тарифов — приходилось сперва активировать
пробную. Добавлена вторичная кнопка «Посмотреть тарифы и купить подписку»
→ /subscription/purchase под карточкой триала на обеих страницах, где она
рендерится: Subscriptions и Dashboard (вход по умолчанию).
Telegram-баг #605056/#605063.
- 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
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`).
Two cabinet routes never showed TrialOfferCard for users without any
subscription when MULTI_TARIFF was enabled:
1. Dashboard (/)
hasNoSubscription was derived from subscriptionResponse, but the
/cabinet/subscription query is disabled in multi-tariff mode
(`enabled: !isMultiTariff`). subscriptionResponse stayed undefined
forever, so `has_subscription === false` was never true and the
trial card never rendered.
Fix: branch on isMultiTariff — use multiSubData.subscriptions.length
when in multi-tariff, keep the single-tariff path unchanged.
2. /subscriptions list
When the array came back empty the page rendered EmptyState directly,
bypassing trial eligibility entirely. Users who navigated to
'Подписки' from the menu before opening the dashboard saw the empty
placeholder and nothing about the trial.
Fix: fetch trial-info on empty state, render TrialOfferCard if
trialInfo.is_available, fall back to EmptyState otherwise. Also
wired the activate mutation locally (mirrors Dashboard) so the
button works without bouncing through the home screen first.
Single-tariff behavior is unchanged. After the fix both entry points
agree: if you have no subscriptions and trial is available, you see
the offer regardless of which page you opened first.
SubscriptionCardActive: passes ?sub= to /connection navigation.
Connection: QR state includes subscriptionId for back-navigation.
ConnectionQR: preserves subscription context on back button.
Dashboard + Subscription: localStorage traffic cache keyed per-subscription
to prevent cooldown bleed between different subscriptions.
- Add SubscriptionListItem and SubscriptionsListResponse types
- Add getSubscriptions/getSubscriptionById API methods
- Add optional subscriptionId parameter to all 30+ API methods
- Create /subscriptions page with subscription list cards
- Add /subscription/:subscriptionId route for per-subscription management
- Dashboard: show multi-tariff navigation block when user has 2+ subscriptions
- Subscription page: read subscriptionId from URL params, pass to all queries
- SubscriptionPurchase: accept subscriptionId from query params
- SubscriptionCardExpired: pass subscription.id to API calls
- Full backward compatibility: without subscriptionId, works as before
- Add is_limited to Subscription type
- Show yellow traffic-exhausted badge on Subscription and AdminUsers pages
- SubscriptionCardExpired: amber theme with warning icon and buy-traffic CTA for limited
- Dashboard: route limited subscriptions to SubscriptionCardExpired
- Subscription page: show additional options (buy traffic) for limited subs
- Use is_limited boolean instead of status string comparison
- Add trafficLimited translations for ru, en, zh, fa
- AdminUserDetail StatusBadge: add limited yellow styling
- Handle expired status in GiftResult (stop polling + show FailedState)
- Add PollErrorState for balance mode poll errors (softer UX)
- Remove non-null assertions in handleSubmit (explicit narrowing)
- Wrap gift routes in ErrorBoundary
- Add pollErrorTitle/pollErrorDesc i18n keys to all 4 locales
- Show SubscriptionCardExpired for disabled daily subscriptions
- Use togglePause() API for resume instead of renewSubscription()
- Add "Suspended" status badge and balance query invalidation
- Add dashboard.suspended and subscription.pause.suspended translations (ru, en, zh, fa)
- WebSocketNotifications: invalidate purchase-options on all 10 balance-affecting events
- Dashboard: invalidate purchase-options after trial activation
- Balance: invalidate purchase-options after payment return and promocode activation
- PromoOffersSection: invalidate purchase-options and balance after promo claim
- Add Subscription tab to desktop header navigation (was only in mobile)
- Fix device dots overflow for large limits (>10) by using progress bar
- Show available referral balance on dashboard instead of total earnings
- Add available_balance and withdrawn fields to ReferralInfo type
Decompose monolithic Dashboard into SubscriptionCardActive,
SubscriptionCardExpired, TrialOfferCard, and StatsGrid sub-components.
Replace Subscription page progress bar with TrafficProgressBar compact.
Add i18n keys for traffic zones, stats labels, expired/trial states.
Replace all bare useAuthStore(), useBlockingStore(), and
useSuccessNotification() calls with individual field selectors.
This prevents components from re-rendering when unrelated store
fields change.
- 21 useAuthStore usages across 20 files: individual selectors
for 1-2 fields, useShallow for 3+ fields
- 5 useBlockingStore usages: individual selectors
- 2 useSuccessNotification usages: individual selectors
Onboarding tutorial was appearing on top of the channel subscription
screen because Dashboard still mounts behind BlockingOverlay.
Added blockingType check to prevent onboarding from triggering.
- Add @kastov/cryptohapp + jsencrypt for encrypted Happ deep links
- Add templateEngine utility to resolve {{SUBSCRIPTION_LINK}},
{{HAPP_CRYPT3_LINK}}, {{HAPP_CRYPT4_LINK}}, {{USERNAME}} templates
- Convert ConnectionModal component into a standalone Connection page
- Add /connection route with ProtectedRoute and lazy loading
- Replace modal invocation in Dashboard and Subscription with
navigate('/connection')
- Add type and svgIconKey optional fields to RemnawaveButton
- Show button type badge and SVG icon in AdminApps BlockCard
- Refactor ThemeTab to use local draft state with Save/Cancel flow
- Move PromoOffersSection higher on dashboard (after subscription card)
- Replace SVG icons with expressive emojis (🏷️ for discounts, 🚀 for test access, 🎁 for general)
- Redesign activate button with gradient, shimmer effect, and hover animation
- Add shadow glow effect to make button more clickable