diff --git a/app/lib/main.dart b/app/lib/main.dart index 1abb6b26d..8a10d16e1 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/screens/app_lifecycle_observer.dart'; import 'package:threebotlogin/screens/splash_screen.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -29,6 +30,11 @@ extension ColorSchemeExtension on ColorScheme { : const Color.fromARGB(255, 10, 10, 10); } +final GlobalKey navigatorKey = GlobalKey(); + +final lastPausedProvider = + StateProvider((ref) => DateTime.now().millisecondsSinceEpoch); + Future main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); @@ -83,60 +89,65 @@ class MyApp extends ConsumerWidget { Theme.of(context).textTheme, ); - return MaterialApp( - theme: ThemeData().copyWith( - colorScheme: kColorScheme, - brightness: Brightness.light, - textTheme: textTheme, - appBarTheme: const AppBarTheme().copyWith( - backgroundColor: kColorScheme.primary, - foregroundColor: kColorScheme.onPrimary, - ), - cardTheme: const CardTheme().copyWith( - color: kColorScheme.surfaceContainer, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - backgroundColor: kColorScheme.primaryContainer), - ), - expansionTileTheme: const ExpansionTileThemeData().copyWith( - backgroundColor: kColorScheme.backgroundDarker, - collapsedBackgroundColor: ThemeData().colorScheme.surface), - bottomNavigationBarTheme: const BottomNavigationBarThemeData().copyWith( - selectedItemColor: kColorScheme.primary, - unselectedItemColor: kColorScheme.secondary, - ), - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: kDarkColorScheme, - brightness: Brightness.dark, - textTheme: textTheme, - appBarTheme: const AppBarTheme().copyWith( - backgroundColor: kDarkColorScheme.primaryContainer, - foregroundColor: kDarkColorScheme.onPrimaryContainer, - ), - cardTheme: const CardTheme().copyWith( - color: kDarkColorScheme.surfaceContainer, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - backgroundColor: kDarkColorScheme.primaryContainer), + return AppLifecycleObserver( + child: MaterialApp( + navigatorKey: navigatorKey, + theme: ThemeData().copyWith( + colorScheme: kColorScheme, + brightness: Brightness.light, + textTheme: textTheme, + appBarTheme: const AppBarTheme().copyWith( + backgroundColor: kColorScheme.primary, + foregroundColor: kColorScheme.onPrimary, + ), + cardTheme: const CardTheme().copyWith( + color: kColorScheme.surfaceContainer, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + backgroundColor: kColorScheme.primaryContainer), + ), + expansionTileTheme: const ExpansionTileThemeData().copyWith( + backgroundColor: kColorScheme.backgroundDarker, + collapsedBackgroundColor: ThemeData().colorScheme.surface), + bottomNavigationBarTheme: + const BottomNavigationBarThemeData().copyWith( + selectedItemColor: kColorScheme.primary, + unselectedItemColor: kColorScheme.secondary, + ), ), - expansionTileTheme: const ExpansionTileThemeData().copyWith( - backgroundColor: kDarkColorScheme.backgroundDarker, - collapsedBackgroundColor: kDarkColorScheme.surface), - bottomNavigationBarTheme: const BottomNavigationBarThemeData().copyWith( - selectedItemColor: kDarkColorScheme.primary, - unselectedItemColor: kDarkColorScheme.secondary, + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: kDarkColorScheme, + brightness: Brightness.dark, + textTheme: textTheme, + appBarTheme: const AppBarTheme().copyWith( + backgroundColor: kDarkColorScheme.primaryContainer, + foregroundColor: kDarkColorScheme.onPrimaryContainer, + ), + cardTheme: const CardTheme().copyWith( + color: kDarkColorScheme.surfaceContainer, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + backgroundColor: kDarkColorScheme.primaryContainer), + ), + expansionTileTheme: const ExpansionTileThemeData().copyWith( + backgroundColor: kDarkColorScheme.backgroundDarker, + collapsedBackgroundColor: kDarkColorScheme.surface), + bottomNavigationBarTheme: + const BottomNavigationBarThemeData().copyWith( + selectedItemColor: kDarkColorScheme.primary, + unselectedItemColor: kDarkColorScheme.secondary, + ), ), + themeMode: themeMode, + home: SplashScreen(initDone: initDone, registered: registered), ), - themeMode: themeMode, - home: SplashScreen(initDone: initDone, registered: registered), ); } } diff --git a/app/lib/screens/app_lifecycle_observer.dart b/app/lib/screens/app_lifecycle_observer.dart new file mode 100644 index 000000000..efd05e08f --- /dev/null +++ b/app/lib/screens/app_lifecycle_observer.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/main.dart'; +import 'package:threebotlogin/screens/home_screen.dart'; + +class AppLifecycleObserver extends ConsumerStatefulWidget { + final Widget child; + const AppLifecycleObserver({super.key, required this.child}); + + @override + ConsumerState createState() => + _AppLifecycleObserverState(); +} + +class _AppLifecycleObserverState extends ConsumerState + with WidgetsBindingObserver { + final int pinCheckTimeout = 60000 * 5; // 5 minute + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + Future.microtask(() { + ref.read(lastPausedProvider.notifier).state = + DateTime.now().millisecondsSinceEpoch; + }); + logger.i('AppLifecycleObserver initialized.'); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + logger.i('AppLifecycleObserver disposed.'); + super.dispose(); + } + + @override + Future didChangeAppLifecycleState(AppLifecycleState state) async { + super.didChangeAppLifecycleState(state); + logger.i('AppLifecycleState changed: $state'); + + final now = DateTime.now().millisecondsSinceEpoch; + final lastPaused = ref.read(lastPausedProvider); + + if (state == AppLifecycleState.paused) { + ref.read(lastPausedProvider.notifier).state = now; + logger.i('App Paused. Last paused time updated: $now'); + } else if (state == AppLifecycleState.resumed) { + if (now - lastPaused >= pinCheckTimeout) { + logger.i('Timeout expired. Requiring authentication.'); + // Navigate to Home Screen + WidgetsBinding.instance.addPostFrameCallback((_) { + navigatorKey.currentState?.push( + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + }); + ref.read(lastPausedProvider.notifier).state = now; + } + } + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 78ffbe34f..b44a63933 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -16,6 +16,7 @@ import 'package:threebotlogin/events/go_wallet_event.dart'; import 'package:threebotlogin/events/new_login_event.dart'; import 'package:threebotlogin/events/uni_link_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/main.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/services/socket_service.dart'; @@ -25,26 +26,30 @@ import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:uni_links/uni_links.dart'; /* Screen shows tab bar and all pages defined in router.dart */ -class HomeScreen extends StatefulWidget { +class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key, this.initialLink, this.backendConnection}); final String? initialLink; final BackendConnection? backendConnection; @override - State createState() => _HomeScreenState(); + ConsumerState createState() => _HomeScreenState(); } -class _HomeScreenState extends State - with WidgetsBindingObserver, SingleTickerProviderStateMixin { +class _HomeScreenState extends ConsumerState + with SingleTickerProviderStateMixin { Globals globals = Globals(); StreamSubscription? _sub; String? initialLink; bool timeoutExpiredInBackground = true; bool pinCheckOpen = false; - int lastCheck = 0; - final int pinCheckTimeout = 60000 * 5; - _HomeScreenState(); + @override + void dispose() { + _sub?.cancel(); + globals.tabController.removeListener(_handleTabSelection); + globals.tabController.dispose(); + super.dispose(); + } void checkPinAndNavigateIfSuccess(int indexIfAuthIsSuccess) async { String? pin = await getPin(); @@ -63,7 +68,8 @@ class _HomeScreenState extends State pinCheckOpen = false; if (authenticated != null && authenticated) { - lastCheck = DateTime.now().millisecondsSinceEpoch; + ref.read(lastPausedProvider.notifier).state = + DateTime.now().millisecondsSinceEpoch; timeoutExpiredInBackground = false; globals.tabController.animateTo(indexIfAuthIsSuccess); } @@ -175,40 +181,6 @@ class _HomeScreenState extends State Events().onEvent(PhoneEvent().runtimeType, (PhoneEvent event) { phoneVerification(context); }); - - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - _sub?.cancel(); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - if (pinCheckOpen) { - return; - } - - int timeSpendWithPausedApp = - DateTime.now().millisecondsSinceEpoch - lastCheck; - - if (timeSpendWithPausedApp >= pinCheckTimeout) { - timeoutExpiredInBackground = true; - } - - if (Globals().router.pinRequired(globals.tabController.index) && - timeoutExpiredInBackground) { - int homeTab = 0; - globals.tabController.animateTo(homeTab); - } - } else if (state == AppLifecycleState.inactive) { - } else if (state == AppLifecycleState.paused) { - lastCheck = DateTime.now().millisecondsSinceEpoch; - } } Future initUniLinks() async { diff --git a/app/lib/widgets/add_farm.dart b/app/lib/widgets/add_farm.dart index a9f0f4264..e2302b441 100644 --- a/app/lib/widgets/add_farm.dart +++ b/app/lib/widgets/add_farm.dart @@ -83,6 +83,10 @@ class _NewFarmState extends State { nameError = "Name can't be empty"; return false; } + if (farmName.contains(' ')) { + nameError = "Name can't contain spaces"; + return false; + } if (widget.isV4) { if (!isAlphanumeric(farmName)) { @@ -108,6 +112,12 @@ class _NewFarmState extends State { }); return false; } + if (_selectedWallet!.stellarBalance == '-1') { + setState(() { + walletError = 'Wallet not activated on stellar'; + }); + return false; + } return true; } @@ -160,7 +170,7 @@ class _NewFarmState extends State { } Future _validateAndAdd() async { - if(!_validateWallet()) return; + if (!_validateWallet()) return; final farmName = _nameController.text.trim(); setState(() { saveLoading = true;