Как мы приручали 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, который превращает обычную почасовую оплату в поток минивыплат в реальном времени. Работодатель депонирует сумму единовременно, а деньги «капают» сотруднику согласно таймеру. От каждого перевода % уходит на адрес рекомендателя.


ссылка на оригинал статьи https://habr.com/ru/articles/908454/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *