Когда мы решили вывести на прод 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/
Добавить комментарий