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.
The 2-column grid inside the max-w-2xl container made each gift card a narrow ~330px half. Gift cards are content-rich (header, code, share preview, buttons), so stack them single-column full-width — they now stretch to the container width on every breakpoint.
- Build the share deep-link as t.me/<bot>?start=GIFT_<code> (literal underscore). The old GIFT%5F encoding was passed to the bot verbatim by Telegram, so its startswith('GIFT_') never matched and activation silently failed.
- Source the bot username from the runtime branding config (env var is only a fallback), so the 'activate via bot' line no longer vanishes on builds without VITE_TELEGRAM_BOT_USERNAME.
- Widen the page on desktop (max-w-2xl) and show sent gifts in a 2-col grid; it was cramped to a mobile column.
- Replace the remaining hand-written SVG icons with the central barrel.
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.
Sweep orange across SubscriptionPurchase/GiftSubscription/PromoOffersSection/
AdminTrafficUsage/AnalyticsTab/withdrawalUtils to warning-* tokens. Stars brand
yellow→orange gradient kept in TopUpAmount.
formatPrice from utils/format.ts only swapped the currency symbol
(220 ₽ -> ¥220) without converting the underlying amount, because it
had no source of exchange rates and the landing/gift pages never called
the useCurrency hook that knew how to fetch them.
- Add a module-level rates cache in utils/format with setExchangeRates
setter. formatPrice now converts kopeks/100 from RUB to the target
currency via currencyApi.convertFromRub when rates are cached.
- useCurrency pushes its loaded rates into that cache so any subsequent
formatPrice call benefits, including subcomponents that cannot easily
receive a prop.
- Call useCurrency in QuickPurchase, GiftSubscription, and
AdminLandingEditor — the only entry points whose subcomponents still
use the synchronous formatPrice (everything else already routes
through useCurrency.formatAmount).
For RU locale behavior is unchanged. For EN/ZH/FA the amount is now
divided by the per-currency rate and formatted via Intl.NumberFormat
with 2 fraction digits (0 for IRR since amounts are large).
The gift page filtered tariffs by selected period, hiding tariffs
whose single period didn't match. When each tariff has a unique
period (30d, 60d, 180d, 360d), only the first tariff was visible.
- Remove period-first filtering (visibleTariffs/allPeriods)
- Show all tariffs directly from config.tariffs
- Auto-select the chosen tariff's period when switching tariffs
- Show tariff cards when more than 1 tariff exists
- Encode underscores as %5F in share URLs (bot link + cabinet link)
to prevent Telegram auto-link detection from stripping trailing _
- Strip GIFT-/GIFT_ prefix from URL code param before pre-filling input
- Use ?? instead of || for prefix stripping to preserve empty strings
- Add varsIgnorePattern to no-unused-vars for destructuring patterns
- Fix all react-hooks/exhaustive-deps by adding missing dependencies
- Refactor useAnimatedNumber to use ref instead of stale state closure
- Wrap handleLinkResult in useCallback for stable deps
- Extract OAuth utilities from OAuthCallback.tsx to utils/oauth.ts
- Extract background config utilities to utils/backgroundConfig.ts
- Remove unused catch parameter in GiftSubscription
- Fix stale initData comparison in clearStaleSessionIfNeeded that destroyed
valid refresh tokens on mobile WebView reopens
- Restrict X-Telegram-Init-Data header to auth endpoints only
- Close Mini App on auth retry to force fresh initData from Telegram
- Merge Connection page error/not-configured states for better UX
- Remove unnecessary comments across 40+ files (section dividers,
restating comments, noise catch block comments)
- Configure ESLint allowEmptyCatch to properly handle intentional
empty catch blocks (62 warnings resolved)
- Use openInvoice() with capabilities.hasInvoice guard for Stars payments
- Fall back to redirect for web environments without invoice support
- Add haptic feedback on payment success/failure
Add promo group banner (green) and active discount banner (orange) to
gift subscription page. PeriodCard now shows original price strikethrough
and discount percentage badge from server-calculated values.
Use createPortal to render toast at document.body level,
escaping framer-motion's transform on the tab content wrapper.
Remove unused useRef import and shareToastTapCopy i18n key.
Remove the full-screen ShareModal dialog that appeared when sharing a
gift code. Replace with a bottom toast that automatically copies the
share message to clipboard on button press and slides up from the
bottom. The toast auto-dismisses after 5 seconds and can be tapped
to copy again.
GIFTCODE_ prefix (9 chars) + full 64-char token = 73 chars,
exceeding Telegram's 64-char start parameter limit. Now all
share links use the same short code shown in the UI (first 12
chars). Backend already supports prefix-based lookup.
- Fix Russian device count declension (_one/_few/_many)
- Show traffic GB and tariff description in buy tab
- Replace copy/share buttons with share modal (bot + cabinet links)
- GiftResult: code-only success state with formatted share message
- Support ?tab=activate&code=TOKEN for auto-activation via cabinet link
- Add is_code_only + purchase_token to API types
- Remove recipient input from buy flow (code-only gifts)
- 3-tab UI: Buy, Activate, My Gifts with animations
- After purchase, switch to My Gifts tab to show gift code
- Gift code display with copy/share buttons
- Activate tab: enter gift code to activate subscription
- My Gifts tab: sent/received gifts with status badges
- Fix: use navigator.language instead of hardcoded ru-RU for dates
- Fix: spread array before sort to prevent React Query cache mutation
- Fix: ARIA tab linkage with proper id/aria-labelledby
- Fix: guard VITE_BOT_USERNAME against undefined in share URLs
- Fix: wrap clipboard/share fallbacks in try/catch
- Add error state to MyGiftsTabContent
- 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