HROSHI: чому «чистий» React без бекенду — це шлях в нікуди (чи ні)

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

SPOILER ALERT: Я особисто перечитував цю статтю кілька разів, але, чесно кажучи, я все ще нічого не розумію в коді, який написав мій CTO Гоголь Джемінаєвіч (Gemini). Тому, якщо там раптом якась повна дичина або «костилі», які змушують ваші очі кровоточити — ткніть мене в це носом у коментарях, я передам Гоголю.

Привіт, DOU! 14 днів тому я шерив останній апдейт по «фінансового антивірусу», який прогнозує касові розриви. Сьогодні — звіт із «полів» бета-тестування.

Перші 25 тестерів (не враховуючи перших 11 альфа-тестерів) вже всередині, і їхній фідбек перетворив мій 14-денний спринт на справжній інтенсив із системного дизайну. Ось що ми встигли напілити в режимі «один фаундер + Gemini» та трохи коду, щоб показати, що там реально «під капотом».

1. Битва з Rate Limits (429 Too Many Requests)

Виявилося, що коли у юзера 5-6 карток у Monobank (чорна, біла, валютні, ФОП), наївний підхід кладе синхронізацію миттєво. Спочатку я зробив класичний Promise.all, і це було фатальною помилкою.

// Як робити НЕ треба
// Це миттєво викликає помилку 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. Питання до спільноти: як би ви реалізували синхронізацію, щоб зберегти парадигму приватності?

  1. Клієнтське шифрування (AES) перед відправкою в Cloud?
  2. Залишити все в LocalStorage і просто реалізувати надійний експорт/імпорт JSON?

Що далі?

Ми продовжуємо ітерувати. Наступна ціль — 50 активних тестерів. Хто хоче подивитися на код (або на те, як він падає) — залітайте на hroshi.app. Вейтліст ще працює.

Чекаю на ваші поради щодо архітектури в коментарях. Йдемо в Cloud чи тримаємо оборону приватності? 👇

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі