Как мы приручали mini‑app telegram: 15 боевых задач и что помогло их решить
Когда мы решили вывести на прод Telegram‑мини‑приложение для «капельных» (stream) TON‑платежей, довольно быстро стало ясно: обычный CRUD‑фронт тут не выживет. Сразу накрыла волна специфичных задач — от гранулярного онбординга в Web‑App до борьбы с ограничениями API‑ключей и тонкостей работы с TON SDK во встроенном браузере Telegram. Каждый шаг требовал не только кода, но и аккуратного выбора архитектурных приёмов, иначе продукту грозили дубли запросов, «белые экраны» и несогласованность состояний.
В этой статье я разобрал пятнадцать самых характерных «боевых» сложностей, показал, каким паттерном мы их укрощали, и какой антипаттерн поджидал за поворотом. Это не академический список, а выжимка из коммитов и ночных дебаг‑сессий, которая поможет тем, кто строит похожие интеграции между Telegram, TON и React.
1. Ручное рукопожатие с Telegram Web‑App
Telegram требует вызвать ready()
и expand()
только после инициализации. Мы завели отдельный useEffect
, который выполняется ровно один раз:
useEffect(() => {
if (window.Telegram?.WebApp) {
window.Telegram.WebApp.ready();
window.Telegram.WebApp.expand();
}
}, []);
Паттерн — Lifecycle hook / Template Method. Чётко отделяем «фазу подключения» от остальной логики.
Антипаттерн — God Effect. Когда в один
useEffect
сваливается и подключение к SDK, и загрузка данных, и подписка на DOM‑события.
2. Защита от «двойного старта» при получении пользователя
При каждом ререндере компонент мог повторно стучаться на /api/add-user
. Простой useRef
‑флаг превратил функцию в Singleton‑guard:
const hasFetched = useRef(false);
const initializeUser = async () => {
if (hasFetched.current) return;
hasFetched.current = true;
/* …дальше идёт fetch… */
};
Паттерн — Singleton + Guard Clause. Позволяет выполнить тяжелую операцию ровно один раз.
Антипаттерн — Double Initialization, из‑за которого на бэкенд летят дубли, а у пользователя мерцает UI.
3. Deep‑link «/start + contract» прямо в детали контракта
Из чата бот передаёт параметр tgWebAppStartParam
. При стартапе мы валидируем роль, ищем ID контракта по адресу и сразу роутим:
const startParam = urlParams.get("tgWebAppStartParam");
if (startParam && res.data.role === "Employee") {
const { id } = await axios.get("/api/get-contract-by-address", { params:{ contractAddress:startParam }});
navigate(`/contract/${id}`);
}
Паттерн — Front Controller (Router). В едином месте intercept‑им url‑параметры и решаем, куда идти.
Антипаттерн — Spaghetti Navigation (ручные
window.location
в компонентах).
4. Под разные сети TON без условных каскадов
Компонент‑стратегия сам подбирает endpoint и API‑key:
export function useTonClient() {
return useAsyncInitialize(async () => {
let endpoint = await getHttpEndpoint({ network: process.env.REACT_APP_NETWORK ?? "testnet" });
if (process.env.REACT_APP_NETWORK === "testnet") {
endpoint = "https://testnet.toncenter.com/api/v2/jsonRPC";
} else {
endpoint = "https://toncenter.com/api/v2/jsonRPC";
}
return new TonClient({ endpoint });
});
}
Паттерн — Strategy. Сеть меняется конфигом, код не трогается.
Антипаттерн — Hard‑coded config. Когда URL меняют руками в нескольких файлах перед релизом.
5. Универсальный хук‑фабрика useAsyncInitialize
Позволяет лениво и единожды инициализировать что угодно — SDK, foreign API, контракт:
export function useAsyncInitialize<T>(fn: () => Promise<T>, deps:any[]=[]){
const [state,setState] = useState<T>()
useEffect(()=>{ (async()=>setState(await fn()))() }, deps)
return state;
}
Паттерн — Lazy Factory. Экономим код и память, создаём объект только когда нужен.
Антипаттерн — Async Call in Render, вызывающий «Cannot update a component while rendering…».
6. Адаптер к Ton Connect: одно лицо вместо трёх SDK
В UI нам нужен просто метод send()
, а не вся тоновская экосистема:
export function useTonConnect(): { sender:Sender } {
const [tonConnectUI] = useTonConnectUI();
return {
sender: {
send: async (args) => {
tonConnectUI.sendTransaction({ messages:[{/* ... */}], validUntil: Date.now()+5*60*1000 });
},
},
};
}
Паттерн — Adapter. UI остаётся неизменным, даже если поменяем SDK.
Антипаттерн — Leaky Abstraction. Когда глубоко вниз протаскивают «сырые» объекты SDK.
7. Отсечка времени на подпись транзакции
Пользователь может уйти; pending TX тогда «висит» вечно. Мы добавили validUntil
:
validUntil: Date.now() + 5 * 60 * 1000 // 5 минут
Паттерн — Timeout / Expiry. Делает UX предсказуемым и упрощает повторную отправку.
Антипаттерн — Infinite Pending Promise. Когда транзакция никогда не закрывается и UI не знает, что делать.
8. Шим Buffer в браузере
Библиотеки crypto из Node требуют global.Buffer
. Один shim во всём приложении:
declare global { interface Window { Buffer: typeof Buffer } }
window.Buffer = Buffer;
Паттерн — Polyfill / Shim. Централизованное решение совместимости.
Антипаттерн — Monkey‑patch Chaos, когда каждый модуль пытается импортировать/переопределять Buffer.
9. Единая тема вместо разноцветного хаоса
Создали ThemeProvider
и конфиг:
const theme = createTheme({
palette:{ primary:{ main:"#1976d2"}, mode:"light"},
typography:{ fontFamily:"Roboto, Arial, sans-serif"},
});
Паттерн — Abstract Factory (Theme Object). Меняем фирменный цвет — меняется всё.
Антипаттерн — Hard‑coded colors, когда дизайнер меняет палитру, а фронт переписывает десятки файлов.
10. «Раковина»‑shell и чистые бизнес‑страницы
Навигация держится в одном месте, каждый экран знает только о своих данных:
<Routes>
<Route path="/" element={!roleSelected ? <WelcomePage/> : …}/>
<Route path="/contracts" element={<AllContractsPage user={user}/>}/>
<Route path="/contract/:id" element={<ContractDetailPage/>}/>
</Routes>
Паттерн — Page Controller (MVVM разделение). Упрощает on‑boarding новых страниц.
Антипаттерн — God Component на 1000 строк JSX.
11. Конечный автомат состояний: роль → кошелёк → главная
Три булевых флага превращаются в два «чистых» состояния:
!roleSelected // ещё не выбрана роль
!user.walletAddress // кошелёк не привязан
/* иначе — главная */
Паттерн — State Machine. Нет «полутонов» (кошелёк есть, но роль не выбрана).
Антипаттерн — Boolean State Explosion. Когда появляется четвёртая комбинация, о которой никто не подумал.
12. Грациозный провал вместо белого экрана
Ошибка сети на старте не роняет всё приложение:
catch (error) {
console.error("Error initializing user:", error);
}
Паттерн — Graceful Degradation / Fail‑safe. Пользователь остаётся в Welcome‑экран, а не видит «Nothing was returned».
Антипаттерн — Fail‑Fast Crash, особенно болезненный на мобильном webview.
13. Gateway к смарт‑контракту вместо прямых вызовов из React
export function useContract(addr:string){
const client = useTonClient();
const finance = useAsyncInitialize(async()=>{
if(!client) return;
return client.open(new Finance(Address.parse(addr)));
},[client]);
return { getConfig: () => finance?.getConfig() };
}
Паттерн — Repository / Gateway. Меняем ABI — правим только этот файл.
Антипаттерн — Anemic Model. Когда методы контракта размазаны по разным компонентам.
14. «Поднять» state, а не раздавать Context направо‑налево
user
и role
хранятся в App
, а дочерние страницы получают их только если нужно:
<AllContractsPage user={user} role={role}/>
Паттерн — Lifting State Up. Простой и прозрачный способ избежать prop‑drilling глубже трёх уровней.
Антипаттерн — Global Mutable Singleton (
window.user
или чрезмерно общий React Context).
15. Тайная жизнь контрактов: асинхронный «бинарник» Finance
Сам контракт открывается лениво, а компоненты получают только чистую функцию getConfig()
— никакой сериализации / десериализации в UI:
Использованный код уже приведён в пункте 13.
Паттерн — Facade. Декодирование, проверка подписи и другие детали спрятаны за «одной ручкой».
Антипаттерн — Leaking Encapsulation, когда в UI начинают парсить BOC‑байты.
Небольшое подведение итогов:
Чёткая архитектура = позволяет быстрее менять бизнес‑логику.
— Переключение сети (testnet ↔ mainnet
) заняло минуты, а не дни, потому что доступ к TON вынесен в Strategy‑слой.Каждая проблема нашла «имя» (паттерн) — упрощает ревью и онбординг.
— Новому разработчику легче понять, зачемuseAsyncInitialize
, когда он видит ссылку на Lazy Factory, а не доморощенный «костыль».Антипаттерны — отличный чек‑лист для code‑review.
— Мы буквально проходились по списку: «Не дублируем ли запрос?», «Не утекла ли абстракция?», «А что будет, если сеть упадёт?».Результат — фронт держит нагрузку, быстро подменяет контракты и переживает «падения» внешних сервисов без белого экрана. Всё это — с минимумом кода‑клонов и максимумом предсказуемости.
Для понимания общего смысла проекта:
Driptonbot — это Telegram‑бот и смарт‑контракт в сети TON, который превращает обычную почасовую оплату в поток минивыплат в реальном времени. Работодатель депонирует сумму единовременно, а деньги «капают» сотруднику согласно таймеру. От каждого перевода % уходит на адрес рекомендателя.