import 'dart:async'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:fluttertoast/fluttertoast.dart'; /// Enhanced Supabase service with auto-connection and silent retry logic class SupabaseService { static SupabaseService? _instance; static SupabaseService get instance => _instance ??= SupabaseService._(); static Timer? _retryTimer; static bool _isInitializing = false; SupabaseService._(); /// Get the Supabase client instance with auto-initialization static SupabaseClient get client { _ensureInitialized(); return Supabase.instance.client; } /// Auto-initialize with environment variables and silent retry static void _ensureInitialized() { if (!_isInitialized() && !_isInitializing) { _initializeWithRetry(); } } /// Check if Supabase is properly initialized static bool _isInitialized() { try { final url = const String.fromEnvironment('SUPABASE_URL'); final key = const String.fromEnvironment('SUPABASE_ANON_KEY'); if (url.isEmpty || key.isEmpty) { return false; } // Check if current client matches the environment variables final currentClient = Supabase.instance.client; return currentClient.headers['Authorization']?.contains(key) ?? false; } catch (e) { return false; } } /// Initialize with automatic retry every 2 seconds until success static void _initializeWithRetry() { if (_isInitializing) return; _isInitializing = true; _retryTimer?.cancel(); _retryTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { try { const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); // Stop and reinitialize once envs are available if (supabaseUrl.isNotEmpty && supabaseAnonKey.isNotEmpty) { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseAnonKey, debug: false, ); timer.cancel(); _isInitializing = false; _retryTimer = null; } } catch (e) { // Silent retry - continue timer } }); } /// Initialize Supabase with environment variables static Future initialize() async { const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) { throw Exception('Missing Supabase credentials'); } await Supabase.initialize( url: supabaseUrl, anonKey: supabaseAnonKey, debug: false, ); } /// Auto-reconnection wrapper for database operations static Future withAutoReconnect(Future Function() operation) async { try { return await operation(); } catch (e) { final errorMessage = e.toString().toLowerCase(); // Check for connection errors if (errorMessage.contains('failed to connect') || errorMessage.contains('networkerror') || errorMessage.contains('invalid key') || errorMessage.contains('connection') || errorMessage.contains('timeout')) { // Wait 500-1000ms debounce before retry await Future.delayed(const Duration(milliseconds: 750)); try { // Reinitialize client await initialize(); // Retry the operation once return await operation(); } catch (retryError) { // Silent failure - operation will return empty results throw retryError; } } // Re-throw non-connection errors throw e; } } /// Validate Supabase credentials and show toast if missing static bool validateCredentials(BuildContext? context) { const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) { Fluttertoast.showToast( msg: "Missing SUPABASE_URL or SUPABASE_ANON_KEY", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return false; } return true; } /// Get truncated Supabase URL for debug display (first/last 8 chars) static String getTruncatedUrl() { const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); if (supabaseUrl.isEmpty) return 'Not configured'; if (supabaseUrl.length <= 16) return supabaseUrl; final first8 = supabaseUrl.substring(0, 8); final last8 = supabaseUrl.substring(supabaseUrl.length - 8); return '$first8...$last8'; } /// Get masked Supabase anon key for debug display (first/last 6 chars, masked middle) static String getMaskedAnonKey() { const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); if (supabaseAnonKey.isEmpty) return 'Not configured'; if (supabaseAnonKey.length <= 12) return '${supabaseAnonKey.substring(0, 3)}***${supabaseAnonKey.substring(supabaseAnonKey.length - 3)}'; final first6 = supabaseAnonKey.substring(0, 6); final last6 = supabaseAnonKey.substring(supabaseAnonKey.length - 6); return '$first6***$last6'; } /// Connection check with lightweight test query static Future> performConnectionCheck() async { try { // 1) Read env vars and validate if (!validateCredentials(null)) { return { 'success': false, 'error': 'Missing SUPABASE_URL or SUPABASE_ANON_KEY', 'count': null, }; } // 2) Initialize Supabase client if needed try { await initialize(); } catch (e) { // Client might already be initialized, continue } // 3) Run lightweight test query exactly as specified: // const { data, error } = await supabase.from('routes').select('id', { head: true, count: 'exact' }); final response = await Supabase.instance.client .from('routes') .select('id') .limit(1); // Using limit instead of head for Flutter supabase client // Get count separately for exact count final countResponse = await Supabase.instance.client.from('routes').select('*'); final count = (countResponse as List).length; return {'success': true, 'error': null, 'count': count}; } catch (e) { return {'success': false, 'error': e.toString(), 'count': null}; } } /// Check if Supabase is initialized bool get isInitialized { try { return Supabase.instance.client.auth.currentUser != null || Supabase.instance.client.auth.currentSession != null || true; // Supabase is initialized even without user } catch (e) { return false; } } /// Force reconnect to Supabase static Future forceReconnect(BuildContext? context) async { try { if (!validateCredentials(context)) { return false; } // Re-initialize Supabase await initialize(); // Test connection with the lightweight query final result = await performConnectionCheck(); if (!result['success']) { Fluttertoast.showToast( msg: "Connection failed: ${result['error']}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return false; } return true; } catch (e) { Fluttertoast.showToast( msg: "Failed to connect to Supabase: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return false; } } }