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)
Two Telegram-Mini-App follow-ups to the backend-unavailable screen:
- Reliable "Close" button (Telegram only) that actually EXITS the Mini App
instead of routing back. closeTelegramApp() tries the legacy
window.Telegram.WebApp.close() global (telegram-web-app.js), then the SDK
closeMiniApp(), then the raw postEvent('web_app_close') — all emit the same
close event, so it can't silently fail. Guarded so the first path never
silently no-ops.
- Eliminate the flash of the /login page before the outage screen. On the
bootstrap path (the app has never reached the backend) reportPossibleBackendDown()
now flips the screen IMMEDIATELY and synchronously instead of waiting on the
confirm probe, so the overlay is up before isLoading flips and /login can never
paint uncovered. Add an eager checkBackendOnStartup() liveness ping at launch
(parallel with auth) so even the no-stored-token / fresh-Telegram path that
makes no early request shows the screen at once. The confirm-probe still guards
already-loaded sessions from a one-off blip.
i18n close key added in ru/en/zh/fa.
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
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)
@telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn.
On Safari < 15.4 and old Android WebViews this method is missing,
causing LaunchParamsRetrieveError and broken initData auth.
Ref: https://github.com/Telegram-Mini-Apps/tma.js/issues/683
- 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)
- Move Google Fonts from CSS @import (render-blocking) to <link> in
index.html with preconnect for earlier discovery
- Remove @import from globals.css — browser no longer waits for CSS
parse -> @import fetch -> font CSS parse before first paint
- Defer initLogoPreload() to requestIdleCallback so logo fetch
doesn't compete with critical resources during page load
Telegram Mini App creates a new WebView on each open, clearing
sessionStorage. Combined with 24h auth_date validation on backend,
returning users couldn't re-authenticate after closing the app.
- Move refresh token to localStorage for persistence across sessions
- Keep access token in sessionStorage (short-lived, OK to lose)
- Fix auth store initialize() to recover when access token is missing
but refresh token exists in localStorage
- Remove separate mountThemeParams() call to fix ConcurrentCallError
(mountMiniApp() handles it internally in SDK v3)
- Update token migration logic for new storage strategy
- Skip token refresh/Bearer header for auth endpoints in request interceptor
- Wait for auth store initialization before attempting Telegram login
- Clear stale session tokens when init data changes (WebView persistence)
- Show backend error details instead of generic messages
- Add /balance/top-up route for payment method selection
- Add /balance/top-up/:methodId route for amount entry and payment
- Remove TopUpModal component (619 lines)
- Simplify InsufficientBalancePrompt to use navigate instead of modals
- Support ?amount and ?returnTo query params for cross-page data flow
- Fix nav isActive to highlight Balance on sub-routes
- Remove unused MainButton platform abstraction
- Increase toast accent opacity for better visibility
При HMR Vite перевыполняет main.tsx, вызывая повторный init() и mount*()
компонентов SDK. mountThemeParams() и другие бросают ConcurrentCallError
асинхронно, что не ловится синхронным try/catch.
Решение: флаг на window.__tg_sdk_initialized предотвращает повторную
инициализацию при горячей перезагрузке модуля.
- Обернуть каждый mount-вызов в safeMountSync() для защиты от
ConcurrentCallError при HMR/повторной инициализации
- Перенести requestFullscreen() внутрь mountViewport().then() — fullscreen
требует смонтированный viewport, ранее вызывался через dynamic import
с setTimeout до завершения mount
- Стабилизировать обработчик BackButton через useRef + useCallback —
navigate меняется каждый рендер, что вызывало постоянную
пере-подписку и потерю обработчика
Полный переход с нативного window.Telegram.WebApp API на @telegram-apps/sdk-react v3.
Bridge SDK оборачивал receiveEvent, из-за чего popup_closed не доходил до нативного
обработчика — это было основной причиной бага с двойным открытием попапа.
Изменения:
- Удалён @telegram-apps/react-router-integration (привязан к SDK v1)
- Инициализация SDK v3 в main.tsx: init(), restoreInitData(), mount всех компонентов
- AppWithNavigator: убран TelegramRouter (initNavigator + useIntegration), все платформы
на BrowserRouter, добавлен TelegramBackButton через SDK v3
- useTelegramSDK: детекция через retrieveLaunchParams(), реактивные значения через
useSignal() (fullscreen, viewport, safe area insets), initData через retrieveRawInitData()
- TelegramAdapter: полная перезапись — попапы через showPopup() (Promise-based, без
ручного onEvent/offEvent/timeout), кнопки через setMainButtonParams/showBackButton,
haptic через hapticFeedbackImpactOccurred, тема через themeParamsState с конвертацией
camelCase→snake_case, cloud storage через getCloudStorageItem/setCloudStorageItem
- api/client.ts: initData через retrieveRawInitData() вместо window.Telegram.WebApp.initData
- AppHeader: фото пользователя через initDataUser() сигнал
- ConnectionModal: openLink через SDK вместо window.Telegram.WebApp.openLink
- ColorPicker: детекция Telegram через isInTelegramWebApp()
- Удалён интерфейс TelegramWebApp (368 строк) и Window augmentation из vite-env.d.ts
- Убрано поле telegram из PlatformContext в types.ts
- Обновлён manualChunks в vite.config.ts
Replace manual BackButton management with official @telegram-apps/react-router-integration
to fix navigation issues (back button cycling and double-click).
Changes:
- Migrate from @tma.js/sdk-react to @telegram-apps/sdk-react
- Downgrade react-router-dom to v6 for package compatibility
- Add AppWithNavigator component with Telegram navigator integration
- Use browser history mode (hashMode: null) to prevent hash URLs
- Add multi-level platform detection (user data, platform, version)
- Add try-catch error handling for navigator attach/detach
- Simplify useBackButton hook to no-op (navigator handles automatically)
- Update vite config with new Telegram packages
The navigator automatically manages BackButton based on router history,
falling back to BrowserRouter in regular browser environment.
Major changes:
- Redesign cabinet with Linear-style components and top navigation
- Replace detail modals with dedicated pages (users, broadcasts, email templates)
- Add email channel support for broadcasts
- Remove pull-to-refresh, improve drag-and-drop on touch devices
- Fix Telegram Mini App: fullscreen, back button, scroll restoration
- Unify admin pages color scheme to design system
- Mobile-first improvements: horizontal tabs for settings, better touch targets