Initial commit: SIBU 2.0 MISSION
This commit is contained in:
255
old/lib/services/supabase_service.dart
Normal file
255
old/lib/services/supabase_service.dart
Normal file
@ -0,0 +1,255 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user