import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'supabase_provider.dart'; final realtimeControllerProvider = ChangeNotifierProvider(( ref, ) { final client = ref.watch(supabaseClientProvider); final controller = RealtimeController(client); ref.onDispose(controller.dispose); return controller; }); /// A lightweight controller that attempts to recover the Supabase Realtime /// connection when the app returns to the foreground or when auth tokens /// are refreshed. class RealtimeController extends ChangeNotifier { final SupabaseClient _client; bool isConnecting = false; bool isFailed = false; String? lastError; int attempts = 0; final int maxAttempts; bool _disposed = false; RealtimeController(this._client, {this.maxAttempts = 4}) { _init(); } void _init() { try { // Listen for auth changes and try to recover the realtime connection _client.auth.onAuthStateChange.listen((data) { final event = data.event; if (event == AuthChangeEvent.tokenRefreshed || event == AuthChangeEvent.signedIn) { recoverConnection(); } }); } catch (e) { debugPrint('RealtimeController._init error: $e'); } } /// Try to reconnect the realtime client using a small exponential backoff. Future recoverConnection() async { if (_disposed) return; if (isConnecting) return; isFailed = false; lastError = null; isConnecting = true; notifyListeners(); try { int delaySeconds = 1; while (attempts < maxAttempts && !_disposed) { attempts++; try { // Best-effort disconnect then connect so the realtime client picks // up any refreshed tokens. try { // Try to refresh session/token if the SDK supports it. Use dynamic // to avoid depending on a specific SDK version symbol. try { await (_client.auth as dynamic).refreshSession?.call(); } catch (_) {} // Best-effort disconnect then connect so the realtime client picks // up any refreshed tokens. The realtime connect/disconnect are // marked internal by the SDK; suppress the lint here since this // is a deliberate best-effort recovery. // ignore: invalid_use_of_internal_member _client.realtime.disconnect(); } catch (_) {} await Future.delayed(const Duration(milliseconds: 300)); try { // ignore: invalid_use_of_internal_member _client.realtime.connect(); } catch (_) {} // Give the socket a moment to stabilise. await Future.delayed(const Duration(seconds: 1)); // Success (best-effort). Reset attempt counter and clear failure. attempts = 0; isFailed = false; lastError = null; break; } catch (e) { lastError = e.toString(); if (attempts >= maxAttempts) { isFailed = true; break; } await Future.delayed(Duration(seconds: delaySeconds)); delaySeconds = delaySeconds * 2; } } } finally { if (!_disposed) { isConnecting = false; notifyListeners(); } } } /// Retry a failed recovery attempt. Future retry() async { if (_disposed) return; attempts = 0; isFailed = false; lastError = null; notifyListeners(); await recoverConnection(); } @override void dispose() { _disposed = true; super.dispose(); } }