492 lines
16 KiB
Dart
492 lines
16 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';
|
|
import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart'
|
|
as intro;
|
|
|
|
// 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');
|
|
final currentUser = Supabase.instance.client.auth.currentUser;
|
|
if (expired && !_showingSessionExpiredDialog && currentUser != null) {
|
|
debugPrint('App: Showing session expired dialog from stream');
|
|
_showSessionExpiredDialog();
|
|
}
|
|
});
|
|
|
|
// Mulai pemeriksaan sesi yang lebih agresif (DISABLED)
|
|
void startAggressiveSessionChecking() {
|
|
debugPrint('App: Session monitoring DISABLED for permissive mode');
|
|
|
|
// Cancel any existing timer
|
|
_sessionCheckTimer?.cancel();
|
|
|
|
// No timer setup - completely disabled
|
|
debugPrint('App: No periodic session checks will run');
|
|
}
|
|
}
|
|
|
|
// Check session validity on startup and periodically (DISABLED)
|
|
Future<void> _checkSessionValidity() async {
|
|
// Session checking DISABLED for permissive mode
|
|
debugPrint('App: Session checking DISABLED for permissive mode');
|
|
return;
|
|
}
|
|
|
|
@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() {
|
|
// Session expired dialog DISABLED for permissive mode
|
|
debugPrint('App: Session expired dialog DISABLED for permissive mode');
|
|
return;
|
|
}
|
|
|
|
// Reset expired dialog state saat login/logout
|
|
void _resetExpiredDialogState() {
|
|
_showingSessionExpiredDialog = false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'TaniSMART',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.green,
|
|
primaryColor: const Color(0xFF056839),
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: const Color(0xFF056839),
|
|
brightness: Brightness.light,
|
|
),
|
|
useMaterial3: true,
|
|
fontFamily: 'Poppins',
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF056839), width: 2),
|
|
),
|
|
),
|
|
),
|
|
home: const intro.AnimatedIntroScreen(),
|
|
routes: Map.from(AppRoutes.routes)..remove('/'),
|
|
// 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');
|
|
});
|
|
}
|