# 2026-09-20 — Мобильное приложение Please Pay Me собрано на iOS-ките ## Запрос «Собери теперь приложение please-pay-me, используя эти компоненты» — то есть поверх Cupertino-кита из прошлой сессии сделать рабочее приложение против существующего API. ## Что сделано ### Слой данных (`lib/data/`) - `models/` — `Budget`, `BudgetStatus` (+ вычисляемые `spentProgress` / `dailyProgress`), `Expense`, `ExpensesPage`, `Job`, `UpcomingPay`, `WeekendPolicy`, `AuthUser`, `AuthSession`. Зеркалят `web/src/types.ts`, парсятся через `models/json.dart` — толерантные коэрсеры (`int`/`double`/`string`), потому что API отдаёт числа из C#-рекордов. - `api/api_client.dart` — транспорт на `http`: Bearer-токен через `tokenProvider`, маппинг `detail`/`title` в `ApiException`, отдельная обработка 401 с колбэком `onUnauthorized` (разлогинивает сессию). - `repositories/` — интерфейсы `BudgetRepository` / `ExpenseRepository` / `JobRepository` / `UserRepository` + REST-реализации на все ручки `MeController`. - `demo/demo_backend.dart` — in-memory бэкенд, повторяющий математику конверта (`dailyLimit = remaining / daysLeft`). Используется в демо-режиме, превью и тестах; `DemoBackend.empty()` — для пустых состояний. ### Ядро (`lib/core/`) - `config/app_config.dart` — `PPM_API_BASE_URL`, `PPM_WEB_URL`, `PPM_DEMO` через `--dart-define`; пустой адрес API автоматически включает демо-режим. - `storage/session_storage.dart` — интерфейс + `SharedPreferences` и in-memory реализации. - `state/async_value.dart` — sealed `AsyncLoading/AsyncData/AsyncError` с `map(...)`, чтобы экраны матчились по состоянию, а не жонглировали тремя nullable-полями. - `format/formatters.dart` — деньги (`1 234,50 ₽`), русские плюрали, «Сегодня/Вчера». ### Состояние (`lib/features/*/`) - `SessionController` — владеет авторизацией и выдаёт репозитории (REST или demo), так что остальное приложение не знает про токены. `sessionKey` пересобирает фичевые контроллеры при входе/выходе. - `BudgetsController` — источник правды по конвертам (список, выбранный, мутации с авто-перезагрузкой, возврат текста ошибки вместо исключения). - `JournalController` — пагинация + группировка по дням + скоуп «текущий/все». - `JobsController` — работы и ближайшая выплата. ### Экраны (`lib/features/`, `lib/app/`) Пять вкладок: Обзор, Журнал, Бюджеты, Работа, Профиль (+ экран входа и сплэш). Формы — модальные шиты: новая/редактирование траты, бюджета, работы; все они принимают `onSubmit`, возвращающий текст ошибки, и не знают про репозитории. `_SessionGate` разводит `restoring / signedOut / signedIn`. ### Кит Удалены демо-экраны `lib/ui/screens/*`; добавлены `AppEmptyState`, `AppErrorView`, `AppLoadingView`, `showAppDatePicker`, `showAppFormSheet`. ### Widgetbook Папка Screens теперь показывает **настоящие экраны** приложения на `DemoScope` (demo-бэкенд + провайдеры), включая пустые состояния, формы и полный таб-бар. ### Авторизация — известное ограничение Telegram Login Widget работает только в вебе. Мобильный клиент принимает JWT из `localStorage.ppm_session_jwt` веб-кабинета и валидирует его через `GET /api/me`. Нативный вход (WebView с Telegram-виджетом либо one-time code через бота) не делался — это отдельное продуктовое решение. ## Тесты - `mobile`: 32 теста — парсинг моделей, форматтеры, все контроллеры на demo-бэкенде, виджет-сценарии (вход, табы, пустые состояния, запись траты end-to-end). - `mobile/widgetbook`: 100 — каждый use-case рендерится в light и dark. - `flutter analyze` по обоим пакетам — 0 issues. - `flutter build windows --debug` проходит (проверка плагинов shared_preferences/url_launcher). ## Запуск ```powershell cd mobile .\run.ps1 # демо-данные .\run.ps1 -ApiBaseUrl https://ppm.example.com ```