MIF_E31222656/lib/main.dart

565 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/core/constants/app_constants.dart';
import 'package:tugas_akhir_supabase/di/service_locator.dart';
import 'package:tugas_akhir_supabase/core/routes/app_routes.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:tugas_akhir_supabase/services/session_manager.dart';
import 'package:tugas_akhir_supabase/services/user_presence_service.dart';
import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart';
import 'package:get_it/get_it.dart';
import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart';
// Tambahkan listener untuk hot reload
bool _hasDoneHotReloadSetup = false;
// Global navigator key for accessing navigation from anywhere
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Fungsi isolate terpisah untuk inisialisasi Supabase
Future<void> _initializeSupabase() async {
try {
await Supabase.initialize(
url: AppConstants.supabaseUrl,
anonKey: AppConstants.supabaseAnonKey,
debug: false,
);
debugPrint('Supabase initialized successfully');
} catch (e) {
debugPrint('Error initializing Supabase: $e');
rethrow;
}
}
void main() async {
// Langsung memulai aplikasi utama
try {
// Initialize Flutter binding
WidgetsFlutterBinding.ensureInitialized();
// Tambahkan penanganan error global
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('Global error handler: $error');
debugPrint('Stack trace: $stack');
// Return true to prevent the error from being reported to the framework
return true;
};
// Tambahkan dukungan untuk hot reload
if (!_hasDoneHotReloadSetup) {
_hasDoneHotReloadSetup = true;
// Set debug flags
debugPrint('======= Setting up hot reload support =======');
// Pastikan semua yang memblokir hot reload dibersihkan
final binding = WidgetsFlutterBinding.ensureInitialized();
binding.addPostFrameCallback((_) {
// Execute after first frame is rendered
debugPrint(
'======= First frame rendered, hot reload should work =======',
);
});
}
// Debug log untuk pelacakan splash screen
debugPrint('======= App Start: Loading TaniSMART application =======');
// Set orientation to portrait
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// Set up error handlers
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('Flutter error: ${details.exception}');
};
// Initialize date formatting
await initializeDateFormatting('id_ID');
await initializeDateFormatting('en_US');
// Initialize Supabase dengan timeout
try {
// Gunakan timeout untuk mencegah blocking terlalu lama
await _initializeSupabase().timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint('Supabase initialization timed out, continuing startup');
throw TimeoutException('Supabase initialization timed out');
},
);
} catch (e) {
// Lanjutkan meskipun ada error, akan ditangani nanti
debugPrint('Continuing after Supabase initialization issue: $e');
}
// Initialize service locator dengan timeout
try {
await initServiceLocator().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('Service locator initialization timed out, continuing');
throw TimeoutException('Service locator initialization timed out');
},
);
} catch (e) {
debugPrint('Continuing after service locator issue: $e');
}
// Initialize session management dengan timeout
try {
await SessionManager.initializeSession().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('Session initialization timed out, continuing');
throw TimeoutException('Session initialization timed out');
},
);
// Initialize user presence service if user is logged in
if (Supabase.instance.client.auth.currentUser != null) {
try {
if (GetIt.instance.isRegistered<UserPresenceService>()) {
await GetIt.instance<UserPresenceService>().initialize();
debugPrint('User presence service initialized');
}
} catch (e) {
debugPrint('Error initializing user presence service: $e');
}
}
} catch (e) {
debugPrint('Continuing after session initialization issue: $e');
}
// Debug log sebelum menjalankan aplikasi
debugPrint(
'======= App initialized: Running TaniSMART application =======',
);
// Run the app
runApp(const RealApp());
} catch (e, stack) {
debugPrint('Error starting full app: $e\n$stack');
// Show error screen
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('TaniSMART Error'),
backgroundColor: Colors.red,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 48),
const SizedBox(height: 16),
const Text(
'Aplikasi tidak dapat dimulai',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Error: ${e.toString()}',
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
main();
},
child: const Text('Coba Lagi'),
),
],
),
),
),
),
),
);
}
}
class RealApp extends StatefulWidget {
const RealApp({super.key});
@override
State<RealApp> createState() => _RealAppState();
}
class _RealAppState extends State<RealApp> with WidgetsBindingObserver {
bool _showingSessionExpiredDialog = false;
bool _isInitialLaunch = true; // Flag untuk menandai initial launch
Timer? _initialLaunchTimer;
StreamSubscription? _sessionSubscription;
Timer? _sessionCheckTimer; // Timer untuk memeriksa sesi secara berkala
bool _hasSetupSessionMonitoring =
false; // Flag baru untuk menandai setup monitoring
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Penundaan yang lebih lama untuk pemasangan listener agar aplikasi bisa dimuat sepenuhnya
Future.delayed(Duration(seconds: 7), () {
if (!mounted) return;
// Hanya setup jika aplikasi masih berjalan dan belum setup
if (!_hasSetupSessionMonitoring) {
_setupSessionMonitoring();
}
});
// Delay pemeriksaan session sampai aplikasi benar-benar siap
// Beri waktu splash screen menyelesaikan animasinya dan navigasi selesai
_initialLaunchTimer = Timer(Duration(seconds: 10), () {
if (mounted) {
setState(() {
_isInitialLaunch =
false; // Reset flag setelah initial launch benar-benar selesai
debugPrint('App: Initial launch phase completed');
});
// Hanya setup jika aplikasi masih berjalan dan belum setup
if (!_hasSetupSessionMonitoring) {
_setupSessionMonitoring();
}
}
});
}
// Metode terpisah untuk setup monitoring sesi
void _setupSessionMonitoring() {
if (_hasSetupSessionMonitoring) return; // Hindari setup duplikat
_hasSetupSessionMonitoring = true;
debugPrint('App: Setting up session expiration listener');
_sessionSubscription = SessionManager.sessionExpiredStream.listen((
expired,
) {
debugPrint('App: Session expired event received: $expired');
if (expired && !_showingSessionExpiredDialog) {
debugPrint('App: Showing session expired dialog from stream');
_showSessionExpiredDialog();
}
});
// Mulai pemeriksaan sesi berkala yang lebih agresif, tapi hanya jika sudah tidak dalam initial launch
if (!_isInitialLaunch) {
_startAggressiveSessionChecking();
}
}
// Mulai pemeriksaan sesi yang lebih agresif
void _startAggressiveSessionChecking() {
debugPrint('App: Starting aggressive session checking');
// Batalkan timer yang ada jika ada
_sessionCheckTimer?.cancel();
// Periksa sesi setiap 15 detik
_sessionCheckTimer = Timer.periodic(Duration(seconds: 15), (timer) {
// Skip jika masih dalam fase initial launch
if (_isInitialLaunch) {
debugPrint('App: Skipping aggressive check during initial launch');
return;
}
// Periksa apakah pengguna sudah login terlebih dahulu
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint(
'App: No authenticated user, skipping aggressive session check',
);
return;
}
debugPrint('App: Running aggressive session check');
_checkSessionValidity();
});
}
// Check session validity on startup and periodically
Future<void> _checkSessionValidity() async {
// Jangan periksa session selama initial launch phase
if (_isInitialLaunch) {
debugPrint('App: Skipping session check during initial launch');
return;
}
try {
debugPrint('App: Checking session validity...');
// Periksa apakah pengguna sudah login terlebih dahulu
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint('App: No authenticated user found, skipping session check');
return;
}
final isValid = await SessionManager.isSessionValid();
debugPrint('App: Session validity check result: $isValid');
if (!isValid && !_showingSessionExpiredDialog) {
debugPrint('App: Session is invalid, showing expired dialog');
_showSessionExpiredDialog();
}
} catch (e) {
debugPrint('App: Error checking session validity: $e');
}
}
@override
void dispose() {
_initialLaunchTimer?.cancel();
_sessionSubscription?.cancel();
_sessionCheckTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
SessionManager.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('App lifecycle state changed to: $state');
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
// App went to background
debugPrint('App: Going to background, calling onAppBackground');
SessionManager.onAppBackground();
break;
case AppLifecycleState.resumed:
// App came to foreground
debugPrint('App: Coming to foreground, calling onAppForeground');
SessionManager.onAppForeground().then((_) {
debugPrint(
'App: After foreground transition, expired = ${SessionManager.isExpired}',
);
// Periksa apakah pengguna sudah login terlebih dahulu
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint(
'App: No authenticated user after foreground, skipping session check',
);
return;
}
if (SessionManager.isExpired && !_showingSessionExpiredDialog) {
debugPrint('App: Session expired after coming to foreground');
_showSessionExpiredDialog();
} else {
// Periksa sesi secara manual untuk memastikan
_checkSessionValidity();
}
});
break;
default:
break;
}
}
void _showSessionExpiredDialog() {
debugPrint('App: Attempting to show session expired dialog');
// Jangan tampilkan dialog jika masih dalam fase initial launch
if (_isInitialLaunch) {
debugPrint(
'App: Still in initial launch phase, skipping session expired dialog',
);
return;
}
if (_showingSessionExpiredDialog) {
debugPrint('App: Dialog already showing, skipping');
return;
}
_showingSessionExpiredDialog = true;
// Pastikan context tersedia
if (navigatorKey.currentContext == null) {
debugPrint('App: Navigator context not available, using delayed dialog');
// Coba lagi setelah beberapa saat
Future.delayed(Duration(milliseconds: 500), () {
if (mounted) _showSessionExpiredDialog();
});
_showingSessionExpiredDialog = false;
return;
}
debugPrint('App: Showing session expired dialog now');
showDialog(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (context) => const SessionExpiredDialog(),
).then((_) {
debugPrint('App: Session expired dialog closed');
_showingSessionExpiredDialog = false;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TaniSMART',
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: const Color(0xFF2E7D32),
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.green,
accentColor: const Color(0xFF66BB6A),
),
scaffoldBackgroundColor: const Color.fromARGB(255, 255, 255, 255),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.black26, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.black26, width: 1.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.black, width: 2.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.red, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.red, width: 2.5),
),
),
dividerColor: Colors.black,
),
routes: AppRoutes.routes,
initialRoute: '/',
// Add navigation observer to clear SnackBars when navigating
navigatorObservers: [
_SnackBarClearingNavigatorObserver(),
_UserInteractionObserver(),
],
// Add router to intercept navigation when session is expired
builder: (context, child) {
// Force login screen if session is expired
if (SessionManager.isExpired &&
child != null &&
!_isInitialLaunch &&
Supabase.instance.client.auth.currentUser != null) {
debugPrint('App: Session expired, forcing login screen');
// Show session expired dialog if not already showing
if (!_showingSessionExpiredDialog) {
// Use a post-frame callback to avoid build phase issues
WidgetsBinding.instance.addPostFrameCallback((_) {
_showSessionExpiredDialog();
});
}
// Return a restricted UI that prevents interaction
return Material(
child: Stack(
children: [
// Blur the background content
Opacity(opacity: 0.3, child: child),
// Show a loading indicator or message
if (!_showingSessionExpiredDialog)
const Center(child: CircularProgressIndicator()),
],
),
);
}
// Normal app flow
return GestureDetector(
onTap: () => _updateUserInteraction(),
onPanDown: (_) => _updateUserInteraction(),
onScaleStart: (_) => _updateUserInteraction(),
behavior: HitTestBehavior.translucent,
child: child!,
);
},
);
}
// Method untuk memperbarui timestamp interaksi pengguna
void _updateUserInteraction() {
debugPrint('App: User interaction detected');
SessionManager.updateLastUserInteraction();
}
}
// Custom navigator observer to clear SnackBars when navigating to new screens
class _SnackBarClearingNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
_clearSnackBars();
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
_clearSnackBars();
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
_clearSnackBars();
}
void _clearSnackBars() {
if (navigatorKey.currentContext != null) {
ScaffoldMessenger.of(navigatorKey.currentContext!).hideCurrentSnackBar();
ScaffoldMessenger.of(navigatorKey.currentContext!).clearSnackBars();
}
}
}
// Observer baru untuk mendeteksi navigasi pengguna sebagai bentuk interaksi
class _UserInteractionObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
debugPrint('App: Navigation interaction detected (push)');
SessionManager.updateLastUserInteraction();
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
debugPrint('App: Navigation interaction detected (pop)');
SessionManager.updateLastUserInteraction();
}
}
// Add a utility function to show the debug FAB from anywhere
void showDebugFAB(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Use the navigator key to navigate safely
navigatorKey.currentState?.pushNamed('/image-test');
});
}