HROSHI: чому «чистий» React без бекенду — це шлях в нікуди (чи ні)
SPOILER ALERT: Я особисто перечитував цю статтю кілька разів, але, чесно кажучи, я все ще нічого не розумію в коді, який написав мій CTO Гоголь Джемінаєвіч (Gemini). Тому, якщо там раптом якась повна дичина або «костилі», які змушують ваші очі кровоточити — ткніть мене в це носом у коментарях, я передам Гоголю.
Привіт, DOU! 14 днів тому я шерив останній апдейт по «фінансового антивірусу», який прогнозує касові розриви. Сьогодні — звіт із «полів» бета-тестування.
Перші 25 тестерів (не враховуючи перших 11 альфа-тестерів) вже всередині, і їхній фідбек перетворив мій
1. Битва з Rate Limits (429 Too Many Requests)
Виявилося, що коли у юзера
// Як робити НЕ треба
// Це миттєво викликає помилку 429 (Too Many Requests) від Mono API
const fetchAllTransactions = async (accounts) => {
try {
const results = await Promise.all(
accounts.map(acc => monoApi.getStatement(acc.id, from, to))
);
return results.flat();
} catch (e) {
console.error("Houston, we have a problem:", e);
}
}
Банк дозволяє отримувати перелік транзакцій не частіше разу на хвилину для кожного рахунку.
Рішення: Реалізували «Smart Sync Queue». Тепер апка оновлює баланси миттєво (через ендпоінт /client-info), а транзакції тягне в черзі з паузою.
// Smart Sync Queue: обробляємо запити послідовно із затримкою
const syncTransactionsQueue = async (accounts, onProgress) => {
const transactions = [];
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
onProgress(`Синхронізую ${account.type} (${i + 1}/${accounts.length})...`);
try {
const txs = await monoApi.getStatement(account.id);
transactions.push(...txs);
// Якщо це не остання картка, чекаємо 65 секунд (із запасом)
if (i < accounts.length - 1) {
await new Promise(resolve => setTimeout(resolve, 65000));
}
} catch (error) {
console.warn(`Skipping account ${account.id}`, error);
}
}
return transactions;
};
2. Мультивалютний хаос
Рахувати Runway (запас міцності) у гривні, коли частина активів у USD, а витрати у PLN — це математичне пекло для фронтенду. Ми інтегрували актуальні курси та додали нормалізацію даних.
// Спрощена логіка конвертації активів у Base Currency (UAH)
const calculateTotalEquity = (accounts, rates) => {
return accounts.reduce((total, acc) => {
if (acc.currencyCode === 980) {
return total + acc.balance;
}
// Шукаємо курс продажу для валюти рахунку (наприклад, USD -> UAH)
const rate = rates.find(r =>
r.currencyCodeA === acc.currencyCode && r.currencyCodeB === 980
);
const balanceInUAH = rate ? (acc.balance * rate.rateBuy) : 0;
return total + balanceInUAH;
}, 0);
};
3. Автоматизація боргів (Debt Calendar)
Тестери просили наочності. Тепер борги автоматично додаються в календар. Це дозволяє Runway Engine бачити ці «міни» задовго до вибуху, просто додаючи їх як планові транзакції у майбутнє.
4. Демо-режим: Trust Issue Solver
Найбільший бар’єр у фінтеху — страх віддавати токен. Ми вирішили це через патерн Adapter. Весь додаток спілкується з інтерфейсом провайдера, і ми просто підміняємо реалізацію на моки.
// Перемикач провайдера даних
const useDataProvider = (isDemoMode) => {
const realApi = useMonoApi();
const mockApi = useMockData(); // Генерує статичні JSON-и
return useMemo(() => {
if (isDemoMode) {
console.log("🛠 Using Mock Data Provider");
return mockApi;
}
return realApi;
}, [isDemoMode]);
};
5. Розширена категоризація транзакцій
МСС-коди часто занадто загальні, тому ми додали шар евристики для уточнення описів.
const enrichTransaction = (tx) => {
let category = mccMap[tx.mcc] || 'other';
// Еверистика: Донати (Monobank Jar)
if (tx.description.toLowerCase().includes('bank jar') || tx.mcc === 4829) {
category = 'donation';
}
// Еверистика: Таксі (Uber/Bolt/Uklon)
if (['uber', 'bolt', 'uklon'].some(s => tx.description.toLowerCase().includes(s))) {
category = 'taxi';
}
return { ...tx, category };
};
Технічна дилема: Local-first vs Cloud
Наразі HROSHI — це «чистий» React-додаток, де IndexedDB — ваш єдиний бекенд.
- Плюс: 100% приватність. Дані не покидають пристрій.
- Мінус: Користувачі бояться чистити кеш браузера.
Зараз ми з Гоголем Джемінаєвичем зважуємо архітектуру переходу на Firestore. Питання до спільноти: як би ви реалізували синхронізацію, щоб зберегти парадигму приватності?
- Клієнтське шифрування (AES) перед відправкою в Cloud?
- Залишити все в LocalStorage і просто реалізувати надійний експорт/імпорт JSON?
Що далі?
Ми продовжуємо ітерувати. Наступна ціль — 50 активних тестерів. Хто хоче подивитися на код (або на те, як він падає) — залітайте на hroshi.app. Вейтліст ще працює.
Чекаю на ваші поради щодо архітектури в коментарях. Йдемо в Cloud чи тримаємо оборону приватності? 👇

Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів