256 lines
7.8 KiB
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;
|
|
}
|
|
}
|
|
}
|