import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:please_pay_me/core/config/app_config.dart'; import 'package:please_pay_me/core/storage/session_storage.dart'; import 'package:please_pay_me/data/api/api_client.dart'; import 'package:please_pay_me/data/demo/demo_backend.dart'; import 'package:please_pay_me/data/models/auth_user.dart'; import 'package:please_pay_me/data/repositories/api_repositories.dart'; import 'package:please_pay_me/data/repositories/repositories.dart'; enum SessionStatus { restoring, signedOut, signedIn } /// Owns authentication and hands out repositories bound to the current /// session, so the rest of the app never sees tokens or base URLs. class SessionController extends ChangeNotifier { SessionController({ required AppConfig config, required SessionStorage storage, http.Client? httpClient, DemoBackend? demoBackend, }) : _config = config, _storage = storage, _httpClient = httpClient, _demo = demoBackend ?? DemoBackend(); final SessionStorage _storage; final http.Client? _httpClient; final DemoBackend _demo; AppConfig _config; SessionStatus _status = SessionStatus.restoring; AuthUser? _user; String? _token; String _baseUrl = ''; bool _isDemo = false; String? _lastError; SessionStatus get status => _status; AuthUser? get user => _user; String get baseUrl => _baseUrl.isEmpty ? _config.apiBaseUrl : _baseUrl; String get webCabinetUrl => _config.webCabinetUrl; /// API address baked in at build time; empty when it must be derived from /// the cabinet URL the user typed. String get configuredApiBaseUrl => _config.apiBaseUrl; bool get isDemo => _isDemo; String? get lastError => _lastError; http.Client? get httpClient => _httpClient; /// Changes whenever the backing data source changes, so feature controllers /// can be rebuilt from scratch on login / logout. String get sessionKey => '${_isDemo ? 'demo' : baseUrl}:${_user?.userId ?? 0}'; BudgetRepository get budgets => _repositories.budgets; ExpenseRepository get expenses => _repositories.expenses; JobRepository get jobs => _repositories.jobs; UserRepository get users => _repositories.users; _Repositories get _repositories { if (_isDemo || _token == null) return _demoRepositories; return _apiRepositories ??= _buildApiRepositories(); } _Repositories? _apiRepositories; late final _Repositories _demoRepositories = _Repositories( budgets: _demo.budgets, expenses: _demo.expenses, jobs: _demo.jobs, users: _demo.users, ); _Repositories _buildApiRepositories() { final client = ApiClient( baseUrl: baseUrl, tokenProvider: () => _token, httpClient: _httpClient, onUnauthorized: signOut, ); return _Repositories( budgets: ApiBudgetRepository(client), expenses: ApiExpenseRepository(client), jobs: ApiJobRepository(client), users: ApiUserRepository(client), ); } Future restore() async { final stored = await _storage.readAll(); final token = stored['token']; final baseUrl = stored['baseUrl']; if (token != null && token.isNotEmpty && baseUrl != null && baseUrl.isNotEmpty) { _token = token; _baseUrl = baseUrl; _isDemo = false; _apiRepositories = null; _user = AuthUser.tryDecode(stored['user']); _status = SessionStatus.signedIn; notifyListeners(); return; } if (_config.demoMode && _config.apiBaseUrl.isEmpty) { _status = SessionStatus.signedOut; notifyListeners(); return; } _status = SessionStatus.signedOut; notifyListeners(); } /// Signs in with a JWT issued by Telegram or Yandex (`POST /api/auth/*`). Future signInWithToken({required String baseUrl, required String token}) async { final normalizedUrl = baseUrl.trim().replaceAll(RegExp(r'/$'), ''); final normalizedToken = token.trim(); if (normalizedUrl.isEmpty || normalizedToken.isEmpty) { _lastError = 'Укажите адрес кабинета и токен'; notifyListeners(); return false; } _lastError = null; _baseUrl = normalizedUrl; _token = normalizedToken; _isDemo = false; _apiRepositories = null; try { final user = await _repositories.users.me(); _user = user; _status = SessionStatus.signedIn; _config = _config.copyWith(apiBaseUrl: normalizedUrl, demoMode: false); await _storage.write( token: normalizedToken, baseUrl: normalizedUrl, user: user.encode(), ); notifyListeners(); return true; } on ApiException catch (error) { _token = null; _apiRepositories = null; _lastError = error.message; _status = SessionStatus.signedOut; notifyListeners(); return false; } } /// Runs the app against [DemoBackend] — no server required. void startDemo() { _isDemo = true; _token = null; _user = DemoBackend.user; _lastError = null; _status = SessionStatus.signedIn; notifyListeners(); } Future signOut() async { await _storage.clear(); _token = null; _user = null; _isDemo = false; _apiRepositories = null; _status = SessionStatus.signedOut; notifyListeners(); } } class _Repositories { const _Repositories({ required this.budgets, required this.expenses, required this.jobs, required this.users, }); final BudgetRepository budgets; final ExpenseRepository expenses; final JobRepository jobs; final UserRepository users; }