Files
tasq/lib/providers/connectivity_provider.dart
T

104 lines
3.8 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart';
import '../brick/cache_warmer.dart';
import '../brick/repository.dart';
import 'stream_recovery.dart';
/// Current online/offline status.
/// Defaults to true so the UI doesn't flash an offline banner on startup.
final isOnlineProvider = StateProvider<bool>((ref) => true);
/// Activating this provider starts a periodic connectivity poll.
/// Watch it from the root widget to keep it alive.
final connectivityMonitorProvider = Provider.autoDispose<void>((ref) {
// Wire up the global online check for StreamRecoveryWrapper so all stream
// wrappers can suppress recovery while offline without per-instance setup.
StreamRecoveryWrapper.setIsOnlineCallback(() => ref.read(isOnlineProvider));
bool previouslyOnline = true;
Future<void> check() async {
final online = await ConnectivityMonitor.check();
final wasOnline = ref.read(isOnlineProvider);
if (online != wasOnline) {
ref.read(isOnlineProvider.notifier).state = online;
}
if (previouslyOnline && !online) {
_onDisconnected();
}
if (!previouslyOnline && online) {
_onReconnected();
}
previouslyOnline = online;
}
// Run immediately, then every 5 seconds.
check();
final timer = Timer.periodic(const Duration(seconds: 5), (_) => check());
ref.onDispose(timer.cancel);
});
/// Pings the app's own backend to determine online status.
class ConnectivityMonitor {
static Future<bool> check() async {
if (kIsWeb) {
// On web the browser enforces CORS: a HEAD request to an external domain
// (tasq.crmc.ph) is blocked even when online, making it look permanently
// offline. Instead probe the Supabase REST endpoint, which is already
// CORS-configured for this app's origin. Any HTTP response (including
// 401 without auth headers) proves the network is reachable.
try {
final restUrl = Supabase.instance.client.rest.url;
final res = await http
.head(Uri.parse(restUrl))
.timeout(const Duration(seconds: 5));
return res.statusCode < 500;
} catch (_) {
return false;
}
}
try {
final res = await http
.head(Uri.parse('https://google.com'))
.timeout(const Duration(seconds: 5));
return res.statusCode < 500;
} catch (_) {
return false;
}
}
}
void _onDisconnected() {
debugPrint('[Connectivity] Went offline — pausing Brick request queue');
// Stop the retry loop so the queue doesn't spam network errors while offline.
// Queued writes are preserved in SQLite and will sync when _onReconnected fires.
AppRepository.offlineQueue?.stop();
}
void _onReconnected() {
debugPrint('[Connectivity] Back online — queued writes will sync via Brick');
if (!AppRepository.isConfigured) return;
// Resume the Brick HTTP queue so queued writes (e.g. offline task creation)
// are replayed against Supabase now that the network is available again.
AppRepository.offlineQueue?.start();
// Restart any Supabase realtime streams that were suppressed while offline.
// Streams that disconnected due to no network are NOT in an error-retry loop
// (recovery was suppressed); this kick-starts them back to live updates.
StreamRecoveryWrapper.notifyAllOnlineRestored();
// Re-warm the Brick cache so the local SQLite mirror reflects any changes
// that happened on the server while we were offline. Fire-and-forget — it
// runs in the background and never throws.
final user = Supabase.instance.client.auth.currentUser;
if (user != null) {
CacheWarmer.warmAll(Supabase.instance.client);
}
}