fix: stabilize auth and home error flows

This commit is contained in:
Artem Kokos
2026-04-27 23:11:45 +07:00
parent c2d7ce5bdc
commit eed04e9122
5 changed files with 247 additions and 41 deletions

View File

@@ -55,6 +55,27 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
await _initApi(home);
}
/// Выбрать дом как активный и сразу проверить auth-state.
/// Если `auth/me` падает, откатываемся к предыдущему дому и auth-state.
Future<void> select(HomeConfig home) async {
final previousHome = state;
final previousAuthState = ref.read(authInfoProvider);
try {
await switchTo(home);
await ref.read(authInfoProvider.notifier).load(failOnError: true);
} catch (error) {
await _restoreSelection(previousHome);
ref.read(authInfoProvider.notifier).restore(previousAuthState);
rethrow;
}
}
Future<void> clear() async {
await ref.read(settingsServiceProvider).setCurrentHomeId(null);
state = null;
}
/// Инициализировать API-клиент текущим домом
Future<void> _initApi(HomeConfig home) async {
final apiKey = await ref
@@ -62,6 +83,18 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
.requireHomeApiKey(home.id);
ref.read(apiProvider).init(home.url, apiKey);
}
Future<void> _restoreSelection(HomeConfig? home) async {
if (home == null) {
await clear();
return;
}
final svc = ref.read(settingsServiceProvider);
await svc.setCurrentHomeId(home.id);
state = home;
await _initApi(home);
}
}
// ─── Список домов ────────────────────────────────────────────
@@ -798,26 +831,35 @@ class ApiKeysNotifier extends Notifier<LoadState<List<ApiKeyInfo>>> {
// ─── Информация об авторизации ────────────────────────────────
final authInfoProvider = NotifierProvider<AuthInfoNotifier, AuthInfo?>(
() => AuthInfoNotifier(),
);
final authInfoProvider =
NotifierProvider<AuthInfoNotifier, LoadState<AuthInfo?>>(
() => AuthInfoNotifier(),
);
class AuthInfoNotifier extends Notifier<AuthInfo?> {
class AuthInfoNotifier extends Notifier<LoadState<AuthInfo?>> {
@override
AuthInfo? build() => null;
LoadState<AuthInfo?> build() => const LoadState.idle(null);
Future<void> load({bool failOnError = false}) async {
Future<AuthInfo?> load({bool failOnError = false}) async {
state = LoadState.loading(state.data);
try {
final api = ref.read(apiProvider);
final res = await api.getAuthMe();
state = AuthInfo.fromApi(res.data);
final authInfo = AuthInfo.fromApi(res.data);
state = LoadState.data(authInfo);
return authInfo;
} catch (e) {
debugPrint("Ошибка загрузки auth/me: $e");
state = LoadState.error(null, describeLoadError(e));
if (failOnError) rethrow;
return null;
}
}
bool get isAdmin => state?.isAdmin == true;
void clear() => state = const LoadState.idle(null);
void restore(LoadState<AuthInfo?> restoredState) => state = restoredState;
bool get isAdmin => state.data?.isAdmin == true;
}
// ─── Геофенс: управление фоновым таском ─────────────────────

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/error_message.dart';
import '../models/home_config.dart';
import '../providers/providers.dart';
import '../services/api_client.dart';
@@ -321,7 +322,7 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
final currentHome = ref.read(currentHomeProvider);
if (currentHome?.id == home.id) {
await ref.read(currentHomeProvider.notifier).switchTo(home);
await ref.read(currentHomeProvider.notifier).select(home);
}
// Синхронизировать фоновый таск с новыми настройками
@@ -331,9 +332,13 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
if (mounted) Navigator.of(context).pop();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Не удалось проверить дом: $e')));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Не удалось сохранить дом: ${describeLoadError(e)}',
),
),
);
}
} finally {
if (mounted) setState(() => _saving = false);

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/error_message.dart';
import '../models/home_config.dart';
import '../providers/providers.dart';
import '../widgets/build_info_text.dart';
@@ -17,6 +18,8 @@ class HomesScreen extends ConsumerStatefulWidget {
class _HomesScreenState extends ConsumerState<HomesScreen> {
late final UserLocationNotifier _userLocationNotifier;
String? _switchingHomeId;
String? _deletingHomeId;
@override
void initState() {
@@ -53,6 +56,9 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
itemBuilder: (context, index) {
final home = homes[index];
final isActive = currentHome?.id == home.id;
final isSwitching = _switchingHomeId == home.id;
final isDeleting = _deletingHomeId == home.id;
final isBusy = isSwitching || isDeleting;
final distKm = location.distanceToKm(
home.latitude,
home.longitude,
@@ -61,6 +67,7 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
enabled: !isBusy,
leading: Icon(
Icons.home,
color: isActive
@@ -87,25 +94,35 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.edit,
size: 20,
color: Colors.white38,
if (isBusy)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
else ...[
IconButton(
icon: const Icon(
Icons.edit,
size: 20,
color: Colors.white38,
),
onPressed: () => _editHome(context, home),
),
onPressed: () => _editHome(context, home),
),
IconButton(
icon: const Icon(
Icons.delete_outline,
size: 20,
color: Colors.redAccent,
IconButton(
icon: const Icon(
Icons.delete_outline,
size: 20,
color: Colors.redAccent,
),
onPressed: () => _confirmDelete(context, home),
),
onPressed: () => _confirmDelete(context, home),
),
],
],
),
onTap: () => _selectHome(context, home),
onTap: isBusy ? null : () => _selectHome(context, home),
),
);
},
@@ -126,9 +143,12 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
}
void _selectHome(BuildContext context, HomeConfig home) async {
if (_switchingHomeId != null || _deletingHomeId != null) return;
setState(() => _switchingHomeId = home.id);
final messenger = ScaffoldMessenger.of(context);
try {
await ref.read(currentHomeProvider.notifier).switchTo(home);
await ref.read(authInfoProvider.notifier).load(failOnError: true);
await ref.read(currentHomeProvider.notifier).select(home);
if (context.mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const RemoteScreen()),
@@ -136,9 +156,17 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Не удалось выбрать дом: $e')));
messenger.showSnackBar(
SnackBar(
content: Text(
'Не удалось выбрать дом: ${describeLoadError(e)}',
),
),
);
}
} finally {
if (mounted) {
setState(() => _switchingHomeId = null);
}
}
}
@@ -169,8 +197,7 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
TextButton(
onPressed: () async {
Navigator.of(ctx).pop();
await ref.read(homesProvider.notifier).remove(home.id);
await syncGeofenceTask(ref.read(homesProvider));
await _deleteHome(context, home);
},
child: const Text(
'Удалить',
@@ -181,6 +208,37 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
),
);
}
Future<void> _deleteHome(BuildContext context, HomeConfig home) async {
if (_switchingHomeId != null || _deletingHomeId != null) return;
final deletedCurrentHome = ref.read(currentHomeProvider)?.id == home.id;
setState(() => _deletingHomeId = home.id);
final messenger = ScaffoldMessenger.of(context);
try {
await ref.read(homesProvider.notifier).remove(home.id);
await ref.read(currentHomeProvider.notifier).load();
if (deletedCurrentHome) {
ref.read(authInfoProvider.notifier).clear();
}
await syncGeofenceTask(ref.read(homesProvider));
} catch (e) {
if (context.mounted) {
messenger.showSnackBar(
SnackBar(
content: Text(
'Не удалось удалить дом: ${describeLoadError(e)}',
),
),
);
}
} finally {
if (mounted) {
setState(() => _deletingHomeId = null);
}
}
}
}
class _EmptyHomesView extends StatelessWidget {

View File

@@ -39,8 +39,8 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
final groups = ref.watch(groupsProvider);
final groupsLoadState = ref.watch(groupsLoadStateProvider);
final currentHome = ref.watch(currentHomeProvider);
final authInfo = ref.watch(authInfoProvider);
final isAdmin = authInfo?.isAdmin == true;
final authInfoState = ref.watch(authInfoProvider);
final isAdmin = authInfoState.data?.isAdmin == true;
return Scaffold(
appBar: AppBar(