01aa362d3c
Previously, if the backend was unreachable at launch, the splash screen routed to the login screen. The server URL remained persisted in SharedPreferences but visually appeared lost, frustrating any user who isn't the service operator. - New AuthStatus.offline distinguishes network failures (NetworkException) from HTTP 401 in AuthNotifier.verify(). - Persist has_ever_logged_in flag on first successful verify/login. - Offline + ever-logged-in lands on the briefing with a sticky offline banner (retry button) instead of being punted to login. - OfflineBanner widget is Tier-2-ready so we can surface "last sync X min ago" once real caching lands (Fable task #147).
73 lines
2.2 KiB
Dart
73 lines
2.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../providers/auth_provider.dart';
|
|
|
|
/// Sticky banner shown when the backend is unreachable. Surfaces a retry
|
|
/// action and reserves space for future "last sync X min ago" messaging once
|
|
/// Tier 2 offline caching lands.
|
|
class OfflineBanner extends ConsumerStatefulWidget {
|
|
const OfflineBanner({super.key});
|
|
|
|
@override
|
|
ConsumerState<OfflineBanner> createState() => _OfflineBannerState();
|
|
}
|
|
|
|
class _OfflineBannerState extends ConsumerState<OfflineBanner> {
|
|
bool _retrying = false;
|
|
|
|
Future<void> _retry() async {
|
|
if (_retrying) return;
|
|
setState(() => _retrying = true);
|
|
try {
|
|
await ref.read(authProvider.notifier).verify();
|
|
} finally {
|
|
if (mounted) setState(() => _retrying = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final status = ref.watch(authProvider);
|
|
if (status != AuthStatus.offline) return const SizedBox.shrink();
|
|
|
|
final scheme = Theme.of(context).colorScheme;
|
|
return Material(
|
|
color: scheme.errorContainer,
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.cloud_off_outlined,
|
|
size: 18, color: scheme.onErrorContainer),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Offline — showing cached data.',
|
|
style: TextStyle(color: scheme.onErrorContainer),
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: _retrying ? null : _retry,
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: scheme.onErrorContainer,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
),
|
|
child: _retrying
|
|
? const SizedBox(
|
|
width: 14,
|
|
height: 14,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|