Files
SIB/old/lib/services/supabase_service.dart

256 lines
7.8 KiB
Dart

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<void> 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<T> withAutoReconnect<T>(Future<T> 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<Map<String, dynamic>> 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<bool> 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;
}
}
}