fix: stabilize auth and home error flows
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
// ─── Геофенс: управление фоновым таском ─────────────────────
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user