feat(app): смена сервера API и очистка данных устройства при выходе

Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
This commit is contained in:
Dmitry
2026-09-19 22:14:14 +03:00
parent bfe74ca47c
commit a559d6de3e
17 changed files with 887 additions and 135 deletions
+49 -2
View File
@@ -1,11 +1,58 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Backend origin. Override at build time:
const _prefsKey = 'api_base_url';
/// Backend origin when the user has not chosen one. Override at build time:
/// `flutter run --dart-define=API_BASE_URL=https://fin.example.com`.
/// On web the default is the page's own origin (Caddy serves app and API together).
String apiBaseUrl() {
String defaultApiBaseUrl() {
const fromEnv = String.fromEnvironment('API_BASE_URL');
if (fromEnv.isNotEmpty) return fromEnv;
if (kIsWeb) return Uri.base.origin;
return 'http://127.0.0.1:8000';
}
/// The address saved on this device, read in `main()` before `runApp`. Loading it up front,
/// rather than lazily like the theme, matters: the session restore fires a request on
/// startup and it must already go to the right server.
Future<String?> loadStoredApiBaseUrl() async =>
(await SharedPreferences.getInstance()).getString(_prefsKey);
/// Overridden in `main()` with [loadStoredApiBaseUrl]; null (tests, nothing saved) means default.
final initialApiBaseUrlProvider = Provider<String?>((_) => null);
/// Backend origin every client is built against. Watched by `apiProvider` and
/// `authDioProvider`, so changing it rebuilds them. To switch servers use
/// `switchApiBaseUrl`: tokens and cached data belong to the old one.
final apiBaseUrlProvider = NotifierProvider<ApiBaseUrlController, String>(
ApiBaseUrlController.new,
);
class ApiBaseUrlController extends Notifier<String> {
@override
String build() => ref.watch(initialApiBaseUrlProvider) ?? defaultApiBaseUrl();
/// [input] as an origin-like URL without a trailing slash, or null if it is not an
/// absolute http(s) URL.
static String? normalize(String input) {
final uri = Uri.tryParse(input.trim());
if (uri == null || !uri.hasAuthority || uri.host.isEmpty) return null;
if (uri.scheme != 'http' && uri.scheme != 'https') return null;
final s = uri.toString();
return s.endsWith('/') ? s.substring(0, s.length - 1) : s;
}
/// Persists [url]; null goes back to [defaultApiBaseUrl].
Future<void> save(String? url) async {
final prefs = await SharedPreferences.getInstance();
if (url == null) {
await prefs.remove(_prefsKey);
state = defaultApiBaseUrl();
} else {
await prefs.setString(_prefsKey, url);
state = url;
}
}
}