Pull to refresh

Как мы приручали mini‑app telegram: 15 боевых задач и что помогло их решить

Level of difficultyMedium
Reading time6 min
Views1.1K

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

Tags:
Hubs:
0
Comments5

Articles