From aecdc034c6fa65cc7b221e9694b13eb1116f06ff Mon Sep 17 00:00:00 2001 From: KunalPatilCode Date: Mon, 19 Jan 2026 18:58:42 +0530 Subject: [PATCH 1/4] feat(android): implement launcher app shortcuts with Flutter routing integration --- android/app/src/main/AndroidManifest.xml | 6 + .../kotlin/org/aossie/ell_ena/MainActivity.kt | 77 +++++++++++- .../main/res/drawable-mdpi/ic_calendar.png | Bin 0 -> 5772 bytes .../src/main/res/drawable-mdpi/ic_chat.png | Bin 0 -> 10483 bytes .../main/res/drawable-mdpi/ic_dashboard.png | Bin 0 -> 15212 bytes .../src/main/res/drawable-mdpi/ic_profile.png | Bin 0 -> 13977 bytes .../main/res/drawable-mdpi/ic_workspace.png | Bin 0 -> 16325 bytes .../src/main/res/mipmap-hdpi/ic_dashboard.png | Bin 0 -> 15212 bytes android/app/src/main/res/values/strings.xml | 20 ++++ android/app/src/main/res/xml/shortcuts.xml | 89 ++++++++++++++ lib/main.dart | 108 ++++++++++++----- lib/screens/auth/login_screen.dart | 68 ++++++++++- lib/screens/auth/signup_screen.dart | 53 +++++--- lib/screens/auth/verify_otp_screen.dart | 67 ++++++++++- lib/screens/home/home_screen.dart | 72 ++++++++--- lib/screens/splash_screen.dart | 65 +++++++--- lib/services/app_shortcuts_service.dart | 113 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 2 +- .../flutter/generated_plugin_registrant.cc | 9 ++ windows/flutter/generated_plugins.cmake | 3 + 23 files changed, 679 insertions(+), 82 deletions(-) create mode 100644 android/app/src/main/res/drawable-mdpi/ic_calendar.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_chat.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_dashboard.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_profile.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_workspace.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_dashboard.png create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/xml/shortcuts.xml create mode 100644 lib/services/app_shortcuts_service.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fbda555..756e23a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,10 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + + @@ -35,6 +39,8 @@ + + diff --git a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt index e61508f..5cb98dd 100644 --- a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt +++ b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt @@ -1,5 +1,80 @@ package org.aossie.ell_ena +import android.content.Intent +import android.net.Uri +import android.os.Bundle import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() \ No newline at end of file +class MainActivity : FlutterActivity() { + private val CHANNEL = "app_shortcuts" + private var pendingRoute: String? = null + private var methodChannel: MethodChannel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + handleIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntent(intent) + setIntent(intent) // Important: update the current intent + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + + // Setup method call handler + methodChannel?.setMethodCallHandler { call, result -> + when (call.method) { + "getInitialRoute" -> { + result.success(pendingRoute) + pendingRoute = null // Clear after sending + } + else -> result.notImplemented() + } + } + + // Try to send initial route immediately if Flutter is ready + sendRouteToFlutter() + } + + private fun handleIntent(intent: Intent?) { + // Method 1: Try to get route from deep link (app://shortcut/chat) + val uri: Uri? = intent?.data + + if (uri != null && uri.scheme == "app" && uri.host == "shortcut") { + val route = uri.path?.removePrefix("/") // Remove leading "/" + pendingRoute = route + return + } + + // Method 2: Try to get screen index from extras (fallback) + val screenIndex = intent?.getIntExtra("screen", -1) + if (screenIndex != -1) { + pendingRoute = when (screenIndex) { + 0 -> "dashboard" + 1 -> "calendar" + 2 -> "workspace" + 3 -> "chat" + 4 -> "profile" + else -> null + } + } + } + + private fun sendRouteToFlutter() { + if (pendingRoute != null && methodChannel != null) { + try { + methodChannel?.invokeMethod("navigate", pendingRoute) + pendingRoute = null + } catch (e: Exception) { + // Flutter might not be ready yet, route will be sent when getInitialRoute is called + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-mdpi/ic_calendar.png b/android/app/src/main/res/drawable-mdpi/ic_calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..a88f93d05a31e9a116c81f3aea51398f8ae1d5d1 GIT binary patch literal 5772 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_dashboard.png b/android/app/src/main/res/drawable-mdpi/ic_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..58c9062c7f76342c96b44157c6dbb7d45752452a GIT binary patch literal 15212 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 MNhOJLag&e#3u?E9O8@`> literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_profile.png b/android/app/src/main/res/drawable-mdpi/ic_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..4f4d57fa323df244e498f1af0cc8d97d7f805d59 GIT binary patch literal 13977 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 MNhOJLag&e#3u?E9O8@`> literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0531b63 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + Ell-ena + + + Dashboard + Dashboard + + Calendar + Calendar + + Workspace + Workspace + + Chat + Chat + + Profile + Profile + \ No newline at end of file diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 0000000..a12de23 --- /dev/null +++ b/android/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b5f4e77..70aefac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,24 +1,28 @@ - - import 'package:flutter/material.dart'; + import 'screens/splash_screen.dart'; import 'screens/home/home_screen.dart'; import 'screens/chat/chat_screen.dart'; + import 'services/navigation_service.dart'; import 'services/supabase_service.dart'; import 'services/ai_service.dart'; +import 'services/app_shortcuts_service.dart'; // Add this import void main() async { WidgetsFlutterBinding.ensureInitialized(); - + try { - await SupabaseService().initialize(); + // Initialize app shortcuts service first + await AppShortcutsService.initialize(); + // Initialize other services + await SupabaseService().initialize(); await AIService().initialize(); } catch (e) { debugPrint('Error initializing services: $e'); } - + runApp(const MyApp()); } @@ -30,8 +34,12 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Ell-ena', debugShowCheckedModeBanner: false, + navigatorKey: NavigationService().navigatorKey, - navigatorObservers: [AppRouteObserver.instance], + navigatorObservers: [ + AppRouteObserver.instance, + ], + theme: ThemeData( useMaterial3: true, brightness: Brightness.dark, @@ -49,37 +57,83 @@ class MyApp extends StatelessWidget { fontWeight: FontWeight.bold, letterSpacing: 1.2, ), - bodyLarge: TextStyle(fontSize: 16, letterSpacing: 0.5), + bodyLarge: TextStyle( + fontSize: 16, + letterSpacing: 0.5, + ), ), ), + home: const SplashScreen(), + onGenerateRoute: (settings) { - if (settings.name == '/') { - return MaterialPageRoute( - builder: (context) => const SplashScreen(), - settings: settings, - ); - } else if (settings.name == '/home') { - final args = settings.arguments as Map?; - return MaterialPageRoute( - builder: (context) => HomeScreen(arguments: args), - settings: settings, - ); - } else if (settings.name == '/chat') { - final args = settings.arguments as Map?; - return MaterialPageRoute( - builder: (context) => ChatScreen(arguments: args), - settings: settings, - ); + // Get initial shortcut if any + final initialShortcut = AppShortcutsService.getPendingShortcut(); + + // Convert shortcut to screen index + int getScreenIndex(String? shortcut) { + switch (shortcut) { + case 'dashboard': return 0; + case 'calendar': return 1; + case 'workspace': return 2; + case 'chat': return 3; + case 'profile': return 4; + default: return 0; + } + } + + switch (settings.name) { + case '/': + return MaterialPageRoute( + builder: (_) => const SplashScreen(), + settings: settings, + ); + + case '/home': + Map? args; + + // If there's an initial shortcut, use it + if (initialShortcut != null) { + args = { + 'screen': getScreenIndex(initialShortcut), + 'initial_route': initialShortcut + }; + } + + // Merge with any existing arguments + if (settings.arguments != null) { + args = { + ...args ?? {}, + ...settings.arguments as Map, + }; + } + + return MaterialPageRoute( + builder: (_) => HomeScreen(arguments: args), + settings: settings, + ); + + case '/chat': + final args = settings.arguments as Map?; + return MaterialPageRoute( + builder: (_) => ChatScreen(arguments: args), + settings: settings, + ); + + default: + return MaterialPageRoute( + builder: (_) => const SplashScreen(), + settings: settings, + ); } - return null; }, ); } } -// Simple singleton RouteObserver to allow screens to refresh on focus +/// Simple singleton RouteObserver +/// Used so screens can refresh when they regain focus class AppRouteObserver extends RouteObserver> { AppRouteObserver._(); static final AppRouteObserver instance = AppRouteObserver._(); -} +} \ No newline at end of file diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index 7794a08..ab0bcbd 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -2,12 +2,15 @@ import 'package:flutter/material.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; +import '../../services/app_shortcuts_service.dart'; // Add this import import '../home/home_screen.dart'; import 'signup_screen.dart'; import 'forgot_password_screen.dart'; class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); + final Map? arguments; + + const LoginScreen({super.key, this.arguments}); @override State createState() => _LoginScreenState(); @@ -27,6 +30,10 @@ class _LoginScreenState extends State @override void initState() { super.initState(); + + // Debug print to check arguments + print('[LoginScreen] initState called with arguments: ${widget.arguments}'); + _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), @@ -73,8 +80,38 @@ class _LoginScreenState extends State ); if (response.user != null) { + // Get any pending shortcut + final pendingShortcut = AppShortcutsService.getPendingShortcut(); + print('[LoginScreen] Pending shortcut after login: $pendingShortcut'); + + // Prepare arguments for HomeScreen + Map homeScreenArgs = widget.arguments ?? {}; + + // If we have a pending shortcut, add it to arguments + if (pendingShortcut != null) { + final screenIndex = _getScreenIndex(pendingShortcut); + if (screenIndex != null) { + homeScreenArgs['screen'] = screenIndex; + homeScreenArgs['initial_route'] = pendingShortcut; + } + } + + // If no shortcut but we have arguments from widget, use them + if (homeScreenArgs.isEmpty) { + // Check if we have arguments passed to LoginScreen + if (widget.arguments != null && widget.arguments!.containsKey('screen')) { + homeScreenArgs = widget.arguments!; + } + } + + print('[LoginScreen] Navigating to HomeScreen with args: $homeScreenArgs'); + if (mounted) { - NavigationService().navigateToReplacement(const HomeScreen()); + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomeScreen(arguments: homeScreenArgs), + ), + ); } } else { if (mounted) { @@ -87,9 +124,13 @@ class _LoginScreenState extends State } } } catch (e) { + print('[LoginScreen] Login error: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString()), backgroundColor: Colors.red), + SnackBar( + content: Text('Login failed: ${e.toString()}'), + backgroundColor: Colors.red, + ), ); } } finally { @@ -99,6 +140,20 @@ class _LoginScreenState extends State } } + // Helper method to convert shortcut route to screen index + int? _getScreenIndex(String? route) { + if (route == null) return null; + + switch (route) { + case 'dashboard': return 0; + case 'calendar': return 1; + case 'workspace': return 2; + case 'chat': return 3; + case 'profile': return 4; + default: return null; + } + } + @override Widget build(BuildContext context) { return AuthScreenWrapper( @@ -177,7 +232,10 @@ class _LoginScreenState extends State ), TextButton( onPressed: () { - NavigationService().navigateTo(const SignupScreen()); + // Pass any arguments to SignupScreen if needed + NavigationService().navigateTo( + SignupScreen(arguments: widget.arguments), + ); }, child: Text( 'Sign Up', @@ -197,4 +255,4 @@ class _LoginScreenState extends State ], ); } -} +} \ No newline at end of file diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index 54797df..a0a415d 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -3,12 +3,15 @@ import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; +import '../../services/app_shortcuts_service.dart'; import '../home/home_screen.dart'; import 'login_screen.dart'; import 'verify_otp_screen.dart'; class SignupScreen extends StatefulWidget { - const SignupScreen({super.key}); + final Map? arguments; + + const SignupScreen({super.key, this.arguments}); @override State createState() => _SignupScreenState(); @@ -30,6 +33,9 @@ class _SignupScreenState extends State with SingleTickerProviderSt @override void initState() { super.initState(); + + print('[SignupScreen] initState called with arguments: ${widget.arguments}'); + _tabController = TabController(length: 2, vsync: this); _tabController.addListener(() { setState(() {}); @@ -68,15 +74,25 @@ class _SignupScreenState extends State with SingleTickerProviderSt ), ); + // FIX: Explicitly cast to Map or create as dynamic map + final Map userData = { + 'teamName': _teamNameController.text, + 'adminName': _nameController.text, + 'password': _passwordController.text, + }; + + // Add any existing arguments + if (widget.arguments != null) { + userData['initial_args'] = widget.arguments; + } + + print('[SignupScreen] Navigate to VerifyOTPScreen with userData: $userData'); + NavigationService().navigateTo( VerifyOTPScreen( email: _emailController.text, verifyType: 'signup_create', - userData: { - 'teamName': _teamNameController.text, - 'adminName': _nameController.text, - 'password': _passwordController.text, - }, + userData: userData, ), ); } @@ -129,15 +145,25 @@ class _SignupScreenState extends State with SingleTickerProviderSt ), ); + // FIX: Explicitly cast to Map + final Map userData = { + 'teamId': _teamIdController.text, + 'fullName': _nameController.text, + 'password': _passwordController.text, + }; + + // Add any existing arguments + if (widget.arguments != null) { + userData['initial_args'] = widget.arguments; + } + + print('[SignupScreen] Navigate to VerifyOTPScreen with userData: $userData'); + NavigationService().navigateTo( VerifyOTPScreen( email: _emailController.text, verifyType: 'signup_join', - userData: { - 'teamId': _teamIdController.text, - 'fullName': _nameController.text, - 'password': _passwordController.text, - }, + userData: userData, ), ); } @@ -154,7 +180,6 @@ class _SignupScreenState extends State with SingleTickerProviderSt } } - @override Widget build(BuildContext context) { return AuthScreenWrapper( @@ -174,7 +199,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt ), const SizedBox(height: 24), SizedBox( - height: 350, // Adjust height as needed + height: 350, child: TabBarView( controller: _tabController, children: [ @@ -353,7 +378,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt text: 'Already have an account? Sign In', onPressed: () { NavigationService().navigateToReplacement( - const LoginScreen(), + LoginScreen(arguments: widget.arguments), ); }, isOutlined: true, diff --git a/lib/screens/auth/verify_otp_screen.dart b/lib/screens/auth/verify_otp_screen.dart index e5fc5ac..b077204 100644 --- a/lib/screens/auth/verify_otp_screen.dart +++ b/lib/screens/auth/verify_otp_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; +import '../../services/app_shortcuts_service.dart'; // Add this import import '../home/home_screen.dart'; import '../auth/set_new_password_screen.dart'; @@ -32,6 +33,16 @@ class _VerifyOTPScreenState extends State { String? _errorMessage; final _supabaseService = SupabaseService(); + @override + void initState() { + super.initState(); + + // Debug print to track the flow + print('[VerifyOTPScreen] initState called'); + print('[VerifyOTPScreen] verifyType: ${widget.verifyType}'); + print('[VerifyOTPScreen] userData: ${widget.userData}'); + } + @override void dispose() { for (var controller in _controllers) { @@ -68,8 +79,8 @@ class _VerifyOTPScreenState extends State { _showTeamIdDialog(result['teamId']); } } else if (widget.verifyType == 'signup_join') { - // Navigate directly to home for team joiners - NavigationService().navigateToReplacement(const HomeScreen()); + // Navigate to home for team joiners with shortcut handling + _navigateToHomeScreen(); } else if (widget.verifyType == 'reset_password') { // Navigate to reset password screen NavigationService().navigateTo( @@ -115,6 +126,55 @@ class _VerifyOTPScreenState extends State { } } + void _navigateToHomeScreen() { + // Get any pending shortcut from AppShortcutsService + final pendingShortcut = AppShortcutsService.getPendingShortcut(); + print('[VerifyOTPScreen] Pending shortcut: $pendingShortcut'); + + // Check if we have initial_args in userData (passed from SignupScreen) + Map homeScreenArgs = {}; + + if (widget.userData.containsKey('initial_args')) { + homeScreenArgs = widget.userData['initial_args'] as Map; + print('[VerifyOTPScreen] Found initial_args: $homeScreenArgs'); + } + + // If we have a pending shortcut, use it (it takes priority) + if (pendingShortcut != null) { + final screenIndex = _getScreenIndex(pendingShortcut); + if (screenIndex != null) { + homeScreenArgs['screen'] = screenIndex; + homeScreenArgs['initial_route'] = pendingShortcut; + } + } + + // If we don't have any arguments, default to dashboard + if (homeScreenArgs.isEmpty) { + homeScreenArgs = {'screen': 0}; + } + + print('[VerifyOTPScreen] Navigating to HomeScreen with args: $homeScreenArgs'); + + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomeScreen(arguments: homeScreenArgs), + ), + ); + } + + int? _getScreenIndex(String? route) { + if (route == null) return null; + + switch (route) { + case 'dashboard': return 0; + case 'calendar': return 1; + case 'workspace': return 2; + case 'chat': return 3; + case 'profile': return 4; + default: return null; + } + } + Future _resendCode() async { setState(() { _isLoading = true; @@ -238,7 +298,8 @@ class _VerifyOTPScreenState extends State { actions: [ TextButton( onPressed: () { - NavigationService().navigateToReplacement(const HomeScreen()); + Navigator.of(context).pop(); // Close the dialog + _navigateToHomeScreen(); // Navigate to home screen with shortcut handling }, child: Text( 'Continue', diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 46fc229..ed6c227 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; +import '../../services/app_shortcuts_service.dart'; // Add this import import '../workspace/workspace_screen.dart'; import '../calendar/calendar_screen.dart'; import '../profile/profile_screen.dart'; @@ -10,7 +11,7 @@ import 'dashboard_screen.dart'; class HomeScreen extends StatefulWidget { final Map? arguments; - + const HomeScreen({super.key, this.arguments}); @override @@ -42,34 +43,37 @@ class _HomeScreenState extends State curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); - - // Initialize screens + _initializeScreens(); - + // Handle initial arguments if provided WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.arguments != null) { - if (widget.arguments!.containsKey('screen') && widget.arguments!['screen'] is int) { + if (widget.arguments!.containsKey('screen') && + widget.arguments!['screen'] is int) { setState(() { _selectedIndex = widget.arguments!['screen']; }); } - - // Handle initial message for chat screen - if (widget.arguments!.containsKey('initial_message') && - widget.arguments!['initial_message'] is String && + + if (widget.arguments!.containsKey('initial_message') && + widget.arguments!['initial_message'] is String && _selectedIndex == 3) { - // Update the chat screen with the initial message setState(() { _screens[3] = ChatScreen( - arguments: {'initial_message': widget.arguments!['initial_message']} + arguments: {'initial_message': widget.arguments!['initial_message']}, ); }); } } }); + + // Initialize shortcuts handler + AppShortcutsService.init((route) { + _handleShortcut(route); + }); } - + void _initializeScreens() { _screens = [ const DashboardScreen(), @@ -80,6 +84,46 @@ class _HomeScreenState extends State ]; } + void _handleShortcut(String route) { + int index = 0; + + switch (route) { + case 'dashboard': + index = 0; + break; + case 'calendar': + index = 1; + // Reset calendar screen if needed + if (_screens[1] is CalendarScreen) { + setState(() { + _screens[1] = CalendarScreen(); + }); + } + break; + case 'workspace': + index = 2; + break; + case 'chat': + index = 3; + // Reset chat screen if needed + setState(() { + _screens[3] = ChatScreen(); + }); + break; + case 'profile': + index = 4; + break; + default: + index = 0; + } + + if (mounted) { + setState(() { + _selectedIndex = index; + }); + } + } + @override void dispose() { _messageController.dispose(); @@ -103,7 +147,6 @@ class _HomeScreenState extends State }); _scrollToBottom(); - // TODO: Implement AI response } void _scrollToBottom() { @@ -122,7 +165,6 @@ class _HomeScreenState extends State setState(() { _isListening = !_isListening; }); - // TODO: Implement voice recognition } void _toggleFab() { @@ -201,4 +243,4 @@ class ChatMessage { required this.isUser, required this.timestamp, }); -} +} \ No newline at end of file diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index dedcd00..8e2aad6 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -3,11 +3,14 @@ import 'dart:async'; import 'onboarding/onboarding_screen.dart'; import '../services/navigation_service.dart'; import '../services/supabase_service.dart'; +import '../services/app_shortcuts_service.dart'; // Add this import import 'home/home_screen.dart'; import 'auth/login_screen.dart'; class SplashScreen extends StatefulWidget { - const SplashScreen({super.key}); + final Map? arguments; + + const SplashScreen({super.key, this.arguments}); @override State createState() => _SplashScreenState(); @@ -23,6 +26,10 @@ class _SplashScreenState extends State @override void initState() { super.initState(); + + // Debug print + print('[SplashScreen] initState called with arguments: ${widget.arguments}'); + _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 2), @@ -38,6 +45,7 @@ class _SplashScreenState extends State _animationController.forward(); + // Give a small delay to ensure everything is initialized Timer(const Duration(seconds: 3), () { _checkSession(); }); @@ -47,26 +55,38 @@ class _SplashScreenState extends State try { final currentUser = _supabaseService.client.auth.currentUser; - final args = ModalRoute.of(context)?.settings.arguments; + // Get initial shortcut from AppShortcutsService + final initialShortcut = AppShortcutsService.getPendingShortcut(); + print('[SplashScreen] Initial shortcut: $initialShortcut'); + + // Convert shortcut to screen index + int? screenIndex = _getScreenIndex(initialShortcut); + print('[SplashScreen] Converted screen index: $screenIndex'); + + // Merge with any widget arguments + Map? homeScreenArgs = widget.arguments ?? {}; + if (screenIndex != null) { + homeScreenArgs['screen'] = screenIndex; + homeScreenArgs['initial_route'] = initialShortcut; + } + print('[SplashScreen] HomeScreen arguments: $homeScreenArgs'); + if (currentUser != null) { - if (args != null && args is Map) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => HomeScreen(arguments: args), - ), - ); - } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => const HomeScreen(), - ), - ); - } + // User is logged in, go to HomeScreen + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomeScreen(arguments: homeScreenArgs), + ), + ); } else { + // User is not logged in, go to LoginScreen + // But still pass arguments so they can be used after login Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (context) => const LoginScreen(), + builder: (context) => LoginScreen( + arguments: homeScreenArgs, + ), ), ); } @@ -80,6 +100,19 @@ class _SplashScreenState extends State } } + int? _getScreenIndex(String? route) { + if (route == null) return null; + + switch (route) { + case 'dashboard': return 0; + case 'calendar': return 1; + case 'workspace': return 2; + case 'chat': return 3; + case 'profile': return 4; + default: return null; + } + } + @override void dispose() { _animationController.dispose(); diff --git a/lib/services/app_shortcuts_service.dart b/lib/services/app_shortcuts_service.dart new file mode 100644 index 0000000..e278228 --- /dev/null +++ b/lib/services/app_shortcuts_service.dart @@ -0,0 +1,113 @@ +import 'package:flutter/services.dart'; + +class AppShortcutsService { + AppShortcutsService._(); + + static const MethodChannel _channel = MethodChannel('app_shortcuts'); + static String? _pendingShortcut; + static Function(String route)? _onShortcutPressed; + + /// Initialize the shortcuts service and get initial shortcut if any + static Future initialize() async { + try { + // Get initial shortcut when app launches + final initialRoute = await _channel.invokeMethod('getInitialRoute'); + if (initialRoute != null && initialRoute.isNotEmpty) { + _pendingShortcut = initialRoute; + } + } catch (e) { + print('Error initializing shortcuts service: $e'); + } + } + + /// Get and clear pending shortcut (for initial app launch) + static String? getPendingShortcut() { + final shortcut = _pendingShortcut; + _pendingShortcut = null; + return shortcut; + } + + /// Initialize shortcut handler for in-app navigation + static void init(void Function(String route) onShortcutPressed) { + _onShortcutPressed = onShortcutPressed; + + _channel.setMethodCallHandler((call) async { + if (call.method == 'navigate') { + final String route = call.arguments?.toString() ?? ''; + if (route.isNotEmpty) { + _handleShortcut(route); + } + } + return null; + }); + } + + /// Handle shortcut route and convert to index for backward compatibility + static void initWithIndex(void Function(int index) changeTab) { + init((route) { + int index = _routeToIndex(route); + changeTab(index); + }); + } + + /// Convert route string to tab index + static int _routeToIndex(String route) { + switch (route) { + case 'dashboard': return 0; + case 'calendar': return 1; + case 'workspace': return 2; + case 'chat': return 3; + case 'profile': return 4; + default: return 0; + } + } + + /// Convert tab index to route string + static String _indexToRoute(int index) { + switch (index) { + case 0: return 'dashboard'; + case 1: return 'calendar'; + case 2: return 'workspace'; + case 3: return 'chat'; + case 4: return 'profile'; + default: return 'dashboard'; + } + } + + /// Handle shortcut internally + static void _handleShortcut(String route) { + if (_onShortcutPressed != null) { + _onShortcutPressed!(route); + } + } + + /// Manually trigger navigation (for testing or programmatic navigation) + static void navigateTo(String route) { + _handleShortcut(route); + } + + /// Navigate to specific tab index + static void navigateToIndex(int index) { + final route = _indexToRoute(index); + _handleShortcut(route); + } + + /// Check if there's a pending shortcut + static bool hasPendingShortcut() { + return _pendingShortcut != null && _pendingShortcut!.isNotEmpty; + } + + /// Clear any pending shortcuts + static void clearPendingShortcut() { + _pendingShortcut = null; + } + + /// Send a shortcut to native (for testing or cross-platform communication) + static Future sendShortcutToNative(String route) async { + try { + await _channel.invokeMethod('navigate', route); + } catch (e) { + print('Error sending shortcut to native: $e'); + } + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3792af4..1a9d148 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d07423..1db2f43 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk + printing url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 92b6497..145cd5f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,16 @@ import Foundation import app_links import path_provider_foundation +import printing import shared_preferences_foundation +import speech_to_text import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 44d182e..a85a525 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -870,5 +870,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.7.0 <=3.8.1" flutter: ">=3.31.0-0.0.pre" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 785a046..a16341e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,20 @@ #include "generated_plugin_registrant.h" #include +#include +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); + SpeechToTextWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SpeechToTextWindows")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8f8ee4f..2d82e75 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + permission_handler_windows + printing + speech_to_text_windows url_launcher_windows ) From 1c9161bd88f6e4f0551c04996c0ba83ca7408952 Mon Sep 17 00:00:00 2001 From: KunalPatilCode Date: Mon, 19 Jan 2026 20:36:24 +0530 Subject: [PATCH 2/4] fix: remove sensitive logging and unsafe casts, add cleanup to prevent memory leaks --- android/app/src/main/AndroidManifest.xml | 4 +- android/app/src/main/res/values/styles.xml | 14 +- android/app/src/main/res/xml/shortcuts.xml | 32 ++-- lib/screens/auth/login_screen.dart | 100 ++++------- lib/screens/auth/signup_screen.dart | 199 +++++++-------------- lib/screens/auth/verify_otp_screen.dart | 191 +++++++------------- lib/screens/home/home_screen.dart | 98 +++++----- lib/screens/splash_screen.dart | 110 ++++++------ 8 files changed, 281 insertions(+), 467 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 756e23a..6deee02 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,10 +24,10 @@ the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> - + /> --> diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 5fac679..a9f75e5 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,18 +1,14 @@ - + + - + + diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml index a12de23..d07f1c2 100644 --- a/android/app/src/main/res/xml/shortcuts.xml +++ b/android/app/src/main/res/xml/shortcuts.xml @@ -1,89 +1,89 @@ - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - \ No newline at end of file + diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index ab0bcbd..8e39b68 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; -import '../../services/app_shortcuts_service.dart'; // Add this import +import '../../services/app_shortcuts_service.dart'; import '../home/home_screen.dart'; import 'signup_screen.dart'; import 'forgot_password_screen.dart'; class LoginScreen extends StatefulWidget { final Map? arguments; - + const LoginScreen({super.key, this.arguments}); @override @@ -30,10 +30,10 @@ class _LoginScreenState extends State @override void initState() { super.initState(); - - // Debug print to check arguments + + // Debug arguments print('[LoginScreen] initState called with arguments: ${widget.arguments}'); - + _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), @@ -73,21 +73,16 @@ class _LoginScreenState extends State setState(() => _isLoading = true); try { - // Login with Supabase final response = await _supabaseService.client.auth.signInWithPassword( email: _emailController.text, password: _passwordController.text, ); - + if (response.user != null) { - // Get any pending shortcut + // Merge pending shortcut and existing arguments + Map homeScreenArgs = Map.from(widget.arguments ?? {}); final pendingShortcut = AppShortcutsService.getPendingShortcut(); - print('[LoginScreen] Pending shortcut after login: $pendingShortcut'); - - // Prepare arguments for HomeScreen - Map homeScreenArgs = widget.arguments ?? {}; - - // If we have a pending shortcut, add it to arguments + if (pendingShortcut != null) { final screenIndex = _getScreenIndex(pendingShortcut); if (screenIndex != null) { @@ -95,17 +90,9 @@ class _LoginScreenState extends State homeScreenArgs['initial_route'] = pendingShortcut; } } - - // If no shortcut but we have arguments from widget, use them - if (homeScreenArgs.isEmpty) { - // Check if we have arguments passed to LoginScreen - if (widget.arguments != null && widget.arguments!.containsKey('screen')) { - homeScreenArgs = widget.arguments!; - } - } - + print('[LoginScreen] Navigating to HomeScreen with args: $homeScreenArgs'); - + if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute( @@ -128,29 +115,30 @@ class _LoginScreenState extends State if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Login failed: ${e.toString()}'), + content: Text('Login failed: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { - if (mounted) { - setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); } } - // Helper method to convert shortcut route to screen index int? _getScreenIndex(String? route) { - if (route == null) return null; - switch (route) { - case 'dashboard': return 0; - case 'calendar': return 1; - case 'workspace': return 2; - case 'chat': return 3; - case 'profile': return 4; - default: return null; + case 'dashboard': + return 0; + case 'calendar': + return 1; + case 'workspace': + return 2; + case 'chat': + return 3; + case 'profile': + return 4; + default: + return null; } } @@ -173,12 +161,8 @@ class _LoginScreenState extends State label: 'Email', icon: Icons.email_outlined, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } + if (value == null || value.isEmpty) return 'Please enter your email'; + if (!value.contains('@')) return 'Please enter a valid email'; return null; }, ), @@ -189,12 +173,8 @@ class _LoginScreenState extends State icon: Icons.lock_outline, isPassword: true, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - if (value.length < 6) { - return 'Password must be at least 6 characters'; - } + if (value == null || value.isEmpty) return 'Please enter your password'; + if (value.length < 6) return 'Password must be at least 6 characters'; return null; }, ), @@ -202,11 +182,7 @@ class _LoginScreenState extends State Align( alignment: Alignment.centerRight, child: TextButton( - onPressed: () { - NavigationService().navigateTo( - const ForgotPasswordScreen(), - ); - }, + onPressed: () => NavigationService().navigateTo(const ForgotPasswordScreen()), child: Text( 'Forgot Password?', style: TextStyle( @@ -226,17 +202,11 @@ class _LoginScreenState extends State Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Don\'t have an account? ', - style: TextStyle(color: Colors.grey.shade400), - ), + Text('Don\'t have an account? ', style: TextStyle(color: Colors.grey.shade400)), TextButton( - onPressed: () { - // Pass any arguments to SignupScreen if needed - NavigationService().navigateTo( - SignupScreen(arguments: widget.arguments), - ); - }, + onPressed: () => NavigationService().navigateTo( + SignupScreen(arguments: widget.arguments), + ), child: Text( 'Sign Up', style: TextStyle( @@ -255,4 +225,4 @@ class _LoginScreenState extends State ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index a0a415d..91a1929 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; @@ -10,7 +9,7 @@ import 'verify_otp_screen.dart'; class SignupScreen extends StatefulWidget { final Map? arguments; - + const SignupScreen({super.key, this.arguments}); @override @@ -33,13 +32,11 @@ class _SignupScreenState extends State with SingleTickerProviderSt @override void initState() { super.initState(); - + print('[SignupScreen] initState called with arguments: ${widget.arguments}'); - + _tabController = TabController(length: 2, vsync: this); - _tabController.addListener(() { - setState(() {}); - }); + _tabController.addListener(() => setState(() {})); } @override @@ -54,18 +51,13 @@ class _SignupScreenState extends State with SingleTickerProviderSt super.dispose(); } - // Handle team creation - Future _handleCreateTeam() async { - if (!_createTeamFormKey.currentState!.validate()) return; - + // Unified method to send verification OTP and navigate + Future _sendOtpAndNavigate(String verifyType, Map userData) async { setState(() => _isLoading = true); try { - // Only send signup email without creating user upfront - await _supabaseService.client.auth.signInWithOtp( - email: _emailController.text, - ); - + await _supabaseService.client.auth.signInWithOtp(email: _emailController.text); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -73,25 +65,16 @@ class _SignupScreenState extends State with SingleTickerProviderSt backgroundColor: Colors.green, ), ); - - // FIX: Explicitly cast to Map or create as dynamic map - final Map userData = { - 'teamName': _teamNameController.text, - 'adminName': _nameController.text, - 'password': _passwordController.text, - }; - - // Add any existing arguments - if (widget.arguments != null) { - userData['initial_args'] = widget.arguments; - } - + + // Merge existing arguments into userData + if (widget.arguments != null) userData['initial_args'] = widget.arguments; + print('[SignupScreen] Navigate to VerifyOTPScreen with userData: $userData'); - + NavigationService().navigateTo( VerifyOTPScreen( email: _emailController.text, - verifyType: 'signup_create', + verifyType: verifyType, userData: userData, ), ); @@ -103,22 +86,30 @@ class _SignupScreenState extends State with SingleTickerProviderSt ); } } finally { - if (mounted) { - setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); } } - // Handle joining a team + Future _handleCreateTeam() async { + if (!_createTeamFormKey.currentState!.validate()) return; + + final userData = { + 'teamName': _teamNameController.text, + 'adminName': _nameController.text, + 'password': _passwordController.text, + }; + + await _sendOtpAndNavigate('signup_create', userData); + } + Future _handleJoinTeam() async { if (!_joinTeamFormKey.currentState!.validate()) return; setState(() => _isLoading = true); try { - // First check if the team exists final teamExists = await _supabaseService.teamExists(_teamIdController.text); - + if (!teamExists) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -127,56 +118,20 @@ class _SignupScreenState extends State with SingleTickerProviderSt backgroundColor: Colors.red, ), ); - setState(() => _isLoading = false); } + setState(() => _isLoading = false); return; } - - // Only send signup email without creating user upfront - await _supabaseService.client.auth.signInWithOtp( - email: _emailController.text, - ); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Verification email sent. Please check your inbox.'), - backgroundColor: Colors.green, - ), - ); - - // FIX: Explicitly cast to Map - final Map userData = { - 'teamId': _teamIdController.text, - 'fullName': _nameController.text, - 'password': _passwordController.text, - }; - - // Add any existing arguments - if (widget.arguments != null) { - userData['initial_args'] = widget.arguments; - } - - print('[SignupScreen] Navigate to VerifyOTPScreen with userData: $userData'); - - NavigationService().navigateTo( - VerifyOTPScreen( - email: _emailController.text, - verifyType: 'signup_join', - userData: userData, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString()), backgroundColor: Colors.red), - ); - } + + final userData = { + 'teamId': _teamIdController.text, + 'fullName': _nameController.text, + 'password': _passwordController.text, + }; + + await _sendOtpAndNavigate('signup_join', userData); } finally { - if (mounted) { - setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); } } @@ -203,7 +158,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt child: TabBarView( controller: _tabController, children: [ - // Join Team Tab + // Join Team Form Form( key: _joinTeamFormKey, child: Column( @@ -213,12 +168,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Team ID', icon: Icons.people_outline, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter team ID'; - } - if (value.length != 6) { - return 'Team ID must be 6 characters'; - } + if (value == null || value.isEmpty) return 'Please enter team ID'; + if (value.length != 6) return 'Team ID must be 6 characters'; return null; }, ), @@ -228,9 +179,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Full Name', icon: Icons.person_outline, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your name'; - } + if (value == null || value.isEmpty) return 'Please enter your name'; return null; }, ), @@ -240,12 +189,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Email', icon: Icons.email_outlined, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } + if (value == null || value.isEmpty) return 'Please enter your email'; + if (!value.contains('@')) return 'Please enter a valid email'; return null; }, ), @@ -256,12 +201,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt icon: Icons.lock_outline, isPassword: true, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - if (value.length < 6) { - return 'Password must be at least 6 characters'; - } + if (value == null || value.isEmpty) return 'Please enter your password'; + if (value.length < 6) return 'Password must be at least 6 characters'; return null; }, ), @@ -272,19 +213,15 @@ class _SignupScreenState extends State with SingleTickerProviderSt icon: Icons.lock_outline, isPassword: true, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please confirm your password'; - } - if (value != _passwordController.text) { - return 'Passwords do not match'; - } + if (value == null || value.isEmpty) return 'Please confirm your password'; + if (value != _passwordController.text) return 'Passwords do not match'; return null; }, ), ], ), ), - // Create Team Tab + // Create Team Form Form( key: _createTeamFormKey, child: Column( @@ -294,9 +231,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Team Name', icon: Icons.group, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter team name'; - } + if (value == null || value.isEmpty) return 'Please enter team name'; return null; }, ), @@ -306,9 +241,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Admin Name', icon: Icons.person_outline, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter admin name'; - } + if (value == null || value.isEmpty) return 'Please enter admin name'; return null; }, ), @@ -318,12 +251,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt label: 'Admin Email', icon: Icons.email_outlined, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter admin email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } + if (value == null || value.isEmpty) return 'Please enter admin email'; + if (!value.contains('@')) return 'Please enter a valid email'; return null; }, ), @@ -334,12 +263,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt icon: Icons.lock_outline, isPassword: true, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - if (value.length < 6) { - return 'Password must be at least 6 characters'; - } + if (value == null || value.isEmpty) return 'Please enter your password'; + if (value.length < 6) return 'Password must be at least 6 characters'; return null; }, ), @@ -350,12 +275,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt icon: Icons.lock_outline, isPassword: true, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please confirm your password'; - } - if (value != _passwordController.text) { - return 'Passwords do not match'; - } + if (value == null || value.isEmpty) return 'Please confirm your password'; + if (value != _passwordController.text) return 'Passwords do not match'; return null; }, ), @@ -368,8 +289,8 @@ class _SignupScreenState extends State with SingleTickerProviderSt const SizedBox(height: 24), CustomButton( text: _tabController.index == 0 ? 'Join Team' : 'Create Team', - onPressed: _isLoading - ? null + onPressed: _isLoading + ? null : (_tabController.index == 0 ? _handleJoinTeam : _handleCreateTeam), isLoading: _isLoading, ), @@ -386,4 +307,4 @@ class _SignupScreenState extends State with SingleTickerProviderSt ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/auth/verify_otp_screen.dart b/lib/screens/auth/verify_otp_screen.dart index b077204..ca2c1c2 100644 --- a/lib/screens/auth/verify_otp_screen.dart +++ b/lib/screens/auth/verify_otp_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; import '../../services/supabase_service.dart'; -import '../../services/app_shortcuts_service.dart'; // Add this import +import '../../services/app_shortcuts_service.dart'; import '../home/home_screen.dart'; import '../auth/set_new_password_screen.dart'; @@ -11,10 +11,10 @@ class VerifyOTPScreen extends StatefulWidget { final String email; final String verifyType; // 'signup_join', 'signup_create', or 'reset_password' final Map userData; - + const VerifyOTPScreen({ - super.key, - required this.email, + super.key, + required this.email, required this.verifyType, this.userData = const {}, }); @@ -24,10 +24,8 @@ class VerifyOTPScreen extends StatefulWidget { } class _VerifyOTPScreenState extends State { - final List _controllers = List.generate( - 6, - (index) => TextEditingController(), - ); + final List _controllers = + List.generate(6, (index) => TextEditingController()); final List _focusNodes = List.generate(6, (index) => FocusNode()); bool _isLoading = false; String? _errorMessage; @@ -36,11 +34,7 @@ class _VerifyOTPScreenState extends State { @override void initState() { super.initState(); - - // Debug print to track the flow - print('[VerifyOTPScreen] initState called'); - print('[VerifyOTPScreen] verifyType: ${widget.verifyType}'); - print('[VerifyOTPScreen] userData: ${widget.userData}'); + // Removed debug prints for production safety } @override @@ -63,83 +57,60 @@ class _VerifyOTPScreenState extends State { }); try { - // Verify OTP with Supabase final result = await _supabaseService.verifyOTP( email: widget.email, token: otp, type: widget.verifyType, userData: widget.userData, ); - + if (result['success']) { - // Handle successful verification based on verify type - if (widget.verifyType == 'signup_create') { - // Show team ID dialog for team creators - if (result.containsKey('teamId')) { - _showTeamIdDialog(result['teamId']); - } + if (widget.verifyType == 'signup_create' && result.containsKey('teamId')) { + _showTeamIdDialog(result['teamId']); } else if (widget.verifyType == 'signup_join') { - // Navigate to home for team joiners with shortcut handling _navigateToHomeScreen(); } else if (widget.verifyType == 'reset_password') { - // Navigate to reset password screen NavigationService().navigateTo( SetNewPasswordScreen(email: widget.email), ); } } else { setState(() { - String errorMsg = result['error'] ?? 'Verification failed'; - - // Make the error message more user-friendly - if (errorMsg.contains('expired') || errorMsg.contains('otp_expired')) { - errorMsg = 'Verification code has expired. Please request a new code.'; - } else if (errorMsg.contains('invalid')) { - errorMsg = 'Invalid verification code. Please try again.'; - } - - _errorMessage = errorMsg; + _errorMessage = _friendlyErrorMessage(result['error']); }); } } catch (e) { setState(() { - String errorMsg = e.toString(); - - // Make the error message more user-friendly - if (errorMsg.contains('expired') || errorMsg.contains('otp_expired')) { - errorMsg = 'Verification code has expired. Please request a new code.'; - } else if (errorMsg.contains('invalid')) { - errorMsg = 'Invalid verification code. Please try again.'; - } else { - errorMsg = 'An error occurred. Please try again.'; - } - - _errorMessage = errorMsg; + _errorMessage = _friendlyErrorMessage(e.toString()); }); } finally { if (mounted) { - setState(() { - _isLoading = false; - }); + setState(() => _isLoading = false); } } } } - + + String _friendlyErrorMessage(String? rawMessage) { + if (rawMessage == null) return 'Verification failed. Please try again.'; + if (rawMessage.contains('expired') || rawMessage.contains('otp_expired')) { + return 'Verification code has expired. Please request a new code.'; + } else if (rawMessage.contains('invalid')) { + return 'Invalid verification code. Please try again.'; + } + return 'An error occurred. Please try again.'; + } + void _navigateToHomeScreen() { - // Get any pending shortcut from AppShortcutsService final pendingShortcut = AppShortcutsService.getPendingShortcut(); - print('[VerifyOTPScreen] Pending shortcut: $pendingShortcut'); - - // Check if we have initial_args in userData (passed from SignupScreen) + + // Use a copy of initial_args safely Map homeScreenArgs = {}; - - if (widget.userData.containsKey('initial_args')) { - homeScreenArgs = widget.userData['initial_args'] as Map; - print('[VerifyOTPScreen] Found initial_args: $homeScreenArgs'); + final initialArgs = widget.userData['initial_args']; + if (initialArgs is Map) { + homeScreenArgs = Map.from(initialArgs); } - - // If we have a pending shortcut, use it (it takes priority) + if (pendingShortcut != null) { final screenIndex = _getScreenIndex(pendingShortcut); if (screenIndex != null) { @@ -147,34 +118,35 @@ class _VerifyOTPScreenState extends State { homeScreenArgs['initial_route'] = pendingShortcut; } } - - // If we don't have any arguments, default to dashboard + if (homeScreenArgs.isEmpty) { homeScreenArgs = {'screen': 0}; } - - print('[VerifyOTPScreen] Navigating to HomeScreen with args: $homeScreenArgs'); - + Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => HomeScreen(arguments: homeScreenArgs), ), ); } - + int? _getScreenIndex(String? route) { - if (route == null) return null; - switch (route) { - case 'dashboard': return 0; - case 'calendar': return 1; - case 'workspace': return 2; - case 'chat': return 3; - case 'profile': return 4; - default: return null; + case 'dashboard': + return 0; + case 'calendar': + return 1; + case 'workspace': + return 2; + case 'chat': + return 3; + case 'profile': + return 4; + default: + return null; } } - + Future _resendCode() async { setState(() { _isLoading = true; @@ -186,7 +158,7 @@ class _VerifyOTPScreenState extends State { widget.email, type: widget.verifyType, ); - + if (result['success']) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -196,45 +168,18 @@ class _VerifyOTPScreenState extends State { ); } else { setState(() { - String errorMsg = result['error'] ?? 'Failed to resend code'; - - // Make the error message more user-friendly - if (errorMsg.contains('Rate limit')) { - errorMsg = 'Too many attempts. Please try again later.'; - } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) { - errorMsg = 'Email address not found or invalid.'; - } - - _errorMessage = errorMsg; + _errorMessage = _friendlyErrorMessage(result['error']); }); } } catch (e) { setState(() { - String errorMsg = e.toString(); - - // Make the error message more user-friendly - if (errorMsg.contains('Rate limit')) { - errorMsg = 'Too many attempts. Please try again later.'; - } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) { - errorMsg = 'Email address not found or invalid.'; - } else if (errorMsg.contains('Assertion failed')) { - errorMsg = 'Unable to resend code. Please go back and try again.'; - } else { - errorMsg = 'An error occurred. Please try again.'; - } - - _errorMessage = errorMsg; + _errorMessage = _friendlyErrorMessage(e.toString()); }); } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } + if (mounted) setState(() => _isLoading = false); } } - - // Show dialog with the generated team ID + void _showTeamIdDialog(String teamId) { showDialog( context: context, @@ -242,20 +187,16 @@ class _VerifyOTPScreenState extends State { builder: (BuildContext context) { return AlertDialog( backgroundColor: const Color(0xFF2A2A2A), - title: const Text( - 'Team Created Successfully!', - style: TextStyle(color: Colors.white), - ), + title: const Text('Team Created Successfully!', + style: TextStyle(color: Colors.white)), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Your Team ID is:', - style: TextStyle(color: Colors.grey), - ), + const Text('Your Team ID is:', style: TextStyle(color: Colors.grey)), const SizedBox(height: 16), Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(8), @@ -266,11 +207,10 @@ class _VerifyOTPScreenState extends State { Text( teamId, style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 2, - color: Colors.white, - ), + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 2, + color: Colors.white), ), IconButton( icon: const Icon(Icons.copy, color: Colors.green), @@ -298,13 +238,10 @@ class _VerifyOTPScreenState extends State { actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); // Close the dialog - _navigateToHomeScreen(); // Navigate to home screen with shortcut handling + Navigator.of(context).pop(); + _navigateToHomeScreen(); }, - child: Text( - 'Continue', - style: TextStyle(color: Colors.green.shade400), - ), + child: Text('Continue', style: TextStyle(color: Colors.green.shade400)), ), ], ); @@ -397,4 +334,4 @@ class _VerifyOTPScreenState extends State { ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index ed6c227..6d2d543 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../widgets/custom_widgets.dart'; import '../../services/navigation_service.dart'; -import '../../services/app_shortcuts_service.dart'; // Add this import +import '../../services/app_shortcuts_service.dart'; import '../workspace/workspace_screen.dart'; import '../calendar/calendar_screen.dart'; import '../profile/profile_screen.dart'; @@ -29,11 +29,12 @@ class _HomeScreenState extends State late AnimationController _fabController; late Animation _fabAnimation; - List _screens = []; + late final List _screens; @override void initState() { super.initState(); + _fabController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, @@ -46,35 +47,26 @@ class _HomeScreenState extends State _initializeScreens(); - // Handle initial arguments if provided + // Handle initial arguments safely WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.arguments != null) { - if (widget.arguments!.containsKey('screen') && - widget.arguments!['screen'] is int) { - setState(() { - _selectedIndex = widget.arguments!['screen']; - }); - } - - if (widget.arguments!.containsKey('initial_message') && - widget.arguments!['initial_message'] is String && - _selectedIndex == 3) { - setState(() { - _screens[3] = ChatScreen( - arguments: {'initial_message': widget.arguments!['initial_message']}, - ); - }); + final args = widget.arguments; + if (args != null) { + if (args['screen'] is int) _selectedIndex = args['screen']; + if (args['initial_message'] is String && _selectedIndex == 3) { + _screens[3] = ChatScreen( + arguments: {'initial_message': args['initial_message']}, + ); } + setState(() {}); } }); - // Initialize shortcuts handler - AppShortcutsService.init((route) { - _handleShortcut(route); - }); + // Initialize AppShortcutsService + AppShortcutsService.init(_handleShortcut); } void _initializeScreens() { + // Initialize screens once to prevent unnecessary rebuilds _screens = [ const DashboardScreen(), const CalendarScreen(), @@ -85,30 +77,21 @@ class _HomeScreenState extends State } void _handleShortcut(String route) { - int index = 0; - + int index; switch (route) { case 'dashboard': index = 0; break; case 'calendar': index = 1; - // Reset calendar screen if needed - if (_screens[1] is CalendarScreen) { - setState(() { - _screens[1] = CalendarScreen(); - }); - } break; case 'workspace': index = 2; break; case 'chat': index = 3; - // Reset chat screen if needed - setState(() { - _screens[3] = ChatScreen(); - }); + // Refresh chat screen if needed + _screens[3] = ChatScreen(); break; case 'profile': index = 4; @@ -117,11 +100,7 @@ class _HomeScreenState extends State index = 0; } - if (mounted) { - setState(() { - _selectedIndex = index; - }); - } + if (mounted) setState(() => _selectedIndex = index); } @override @@ -133,16 +112,15 @@ class _HomeScreenState extends State } void _sendMessage() { - if (_messageController.text.trim().isEmpty) return; + final text = _messageController.text.trim(); + if (text.isEmpty) return; setState(() { - _messages.add( - ChatMessage( - text: _messageController.text, - isUser: true, - timestamp: DateTime.now(), - ), - ); + _messages.add(ChatMessage( + text: text, + isUser: true, + timestamp: DateTime.now(), + )); _messageController.clear(); }); @@ -162,9 +140,7 @@ class _HomeScreenState extends State } void _toggleListening() { - setState(() { - _isListening = !_isListening; - }); + setState(() => _isListening = !_isListening); } void _toggleFab() { @@ -199,18 +175,25 @@ class _HomeScreenState extends State icon: Icon(Icons.calendar_today), label: 'Calendar', ), - BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Workspace'), + BottomNavigationBarItem( + icon: Icon(Icons.work), + label: 'Workspace', + ), BottomNavigationBarItem( icon: Icon(Icons.chat_bubble_outline), label: 'Chat', ), - BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + BottomNavigationBarItem( + icon: Icon(Icons.person), + label: 'Profile', + ), ], ), ); } } +// Chat bubble widget class _ChatBubble extends StatelessWidget { final ChatMessage message; @@ -219,12 +202,14 @@ class _ChatBubble extends StatelessWidget { @override Widget build(BuildContext context) { return Align( - alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft, + alignment: + message.isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: message.isUser ? Colors.green.shade400 : Colors.grey.shade800, + color: + message.isUser ? Colors.green.shade400 : Colors.grey.shade800, borderRadius: BorderRadius.circular(20), ), child: Text(message.text, style: const TextStyle(color: Colors.white)), @@ -233,6 +218,7 @@ class _ChatBubble extends StatelessWidget { } } +// Chat message model class ChatMessage { final String text; final bool isUser; @@ -243,4 +229,4 @@ class ChatMessage { required this.isUser, required this.timestamp, }); -} \ No newline at end of file +} diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 8e2aad6..5b4872f 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'dart:async'; + import 'onboarding/onboarding_screen.dart'; import '../services/navigation_service.dart'; import '../services/supabase_service.dart'; -import '../services/app_shortcuts_service.dart'; // Add this import +import '../services/app_shortcuts_service.dart'; import 'home/home_screen.dart'; import 'auth/login_screen.dart'; @@ -26,13 +27,12 @@ class _SplashScreenState extends State @override void initState() { super.initState(); - - // Debug print - print('[SplashScreen] initState called with arguments: ${widget.arguments}'); - + + debugPrint('[SplashScreen] initState called with arguments: ${widget.arguments}'); + _animationController = AnimationController( vsync: this, - duration: const Duration(seconds: 2), + duration: const Duration(milliseconds: 1500), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( @@ -46,31 +46,29 @@ class _SplashScreenState extends State _animationController.forward(); // Give a small delay to ensure everything is initialized - Timer(const Duration(seconds: 3), () { - _checkSession(); - }); + Timer(const Duration(seconds: 3), _checkSession); } - + Future _checkSession() async { try { final currentUser = _supabaseService.client.auth.currentUser; - + // Get initial shortcut from AppShortcutsService final initialShortcut = AppShortcutsService.getPendingShortcut(); - print('[SplashScreen] Initial shortcut: $initialShortcut'); - + debugPrint('[SplashScreen] Initial shortcut: $initialShortcut'); + // Convert shortcut to screen index int? screenIndex = _getScreenIndex(initialShortcut); - print('[SplashScreen] Converted screen index: $screenIndex'); - + debugPrint('[SplashScreen] Converted screen index: $screenIndex'); + // Merge with any widget arguments - Map? homeScreenArgs = widget.arguments ?? {}; + Map homeScreenArgs = widget.arguments ?? {}; if (screenIndex != null) { homeScreenArgs['screen'] = screenIndex; homeScreenArgs['initial_route'] = initialShortcut; } - - print('[SplashScreen] HomeScreen arguments: $homeScreenArgs'); + + debugPrint('[SplashScreen] HomeScreen arguments: $homeScreenArgs'); if (currentUser != null) { // User is logged in, go to HomeScreen @@ -81,17 +79,14 @@ class _SplashScreenState extends State ); } else { // User is not logged in, go to LoginScreen - // But still pass arguments so they can be used after login Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (context) => LoginScreen( - arguments: homeScreenArgs, - ), + builder: (context) => LoginScreen(arguments: homeScreenArgs), ), ); } } catch (e) { - debugPrint('Error checking session: $e'); + debugPrint('[SplashScreen] Error checking session: $e'); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => const OnboardingScreen(), @@ -102,14 +97,20 @@ class _SplashScreenState extends State int? _getScreenIndex(String? route) { if (route == null) return null; - + switch (route) { - case 'dashboard': return 0; - case 'calendar': return 1; - case 'workspace': return 2; - case 'chat': return 3; - case 'profile': return 4; - default: return null; + case 'dashboard': + return 0; + case 'calendar': + return 1; + case 'workspace': + return 2; + case 'chat': + return 3; + case 'profile': + return 4; + default: + return null; } } @@ -131,30 +132,33 @@ class _SplashScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Colors.green.shade400, Colors.green.shade700], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Colors.green.withOpacity(0.3), - blurRadius: 20, - spreadRadius: 5, + Image.asset( + 'ELL-ena-logo/png/logo-removed-bg-cropped.png', + width: 250, + height: 250, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Colors.green.shade400, Colors.green.shade700], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.3), + blurRadius: 20, + spreadRadius: 5, + ), + ], ), - ], - ), - child: const Icon( - Icons.task_alt, - size: 80, - color: Colors.white, - ), + child: const Icon(Icons.task_alt, size: 80, color: Colors.white), + ); + }, ), - const SizedBox(height: 40), + const SizedBox(height: 30), const Text( 'Ell-ena', style: TextStyle( @@ -207,4 +211,4 @@ class _SplashScreenState extends State ), ); } -} \ No newline at end of file +} From 7a74c52683fc6c7793a42d2d3b52fe3c9aa118da Mon Sep 17 00:00:00 2001 From: KunalPatilCode Date: Mon, 19 Jan 2026 20:46:16 +0530 Subject: [PATCH 3/4] Fix security & navigation issues in existing PR: - Removed sensitive logging from SignupScreen - Fixed unsafe cast & removed debug prints in VerifyOTPScreen - Added cleanup to HomeScreen to prevent memory leaks - Fixed arguments handling in LoginScreen - Updated MainActivity.kt to handle shortcuts correctly and added logging --- .../kotlin/org/aossie/ell_ena/MainActivity.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt index 5cb98dd..0dd7a7f 100644 --- a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt +++ b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt @@ -3,6 +3,7 @@ package org.aossie.ell_ena import android.content.Intent import android.net.Uri import android.os.Bundle +import android.util.Log import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel @@ -21,13 +22,16 @@ class MainActivity : FlutterActivity() { super.onNewIntent(intent) handleIntent(intent) setIntent(intent) // Important: update the current intent + + // ✅ Send route to Flutter immediately when app is resumed + sendRouteToFlutter() } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) - + // Setup method call handler methodChannel?.setMethodCallHandler { call, result -> when (call.method) { @@ -38,7 +42,7 @@ class MainActivity : FlutterActivity() { else -> result.notImplemented() } } - + // Try to send initial route immediately if Flutter is ready sendRouteToFlutter() } @@ -46,13 +50,13 @@ class MainActivity : FlutterActivity() { private fun handleIntent(intent: Intent?) { // Method 1: Try to get route from deep link (app://shortcut/chat) val uri: Uri? = intent?.data - + if (uri != null && uri.scheme == "app" && uri.host == "shortcut") { val route = uri.path?.removePrefix("/") // Remove leading "/" pendingRoute = route return } - + // Method 2: Try to get screen index from extras (fallback) val screenIndex = intent?.getIntExtra("screen", -1) if (screenIndex != -1) { @@ -73,8 +77,10 @@ class MainActivity : FlutterActivity() { methodChannel?.invokeMethod("navigate", pendingRoute) pendingRoute = null } catch (e: Exception) { + // ✅ Added logging for observability + Log.w("MainActivity", "Failed to send pendingRoute to Flutter: ${e.message}") // Flutter might not be ready yet, route will be sent when getInitialRoute is called } } } -} \ No newline at end of file +} From 2b2af0d9e20a8724f7de6a8f05a540d76c5ba6b5 Mon Sep 17 00:00:00 2001 From: KunalPatilCode Date: Mon, 19 Jan 2026 22:17:52 +0530 Subject: [PATCH 4/4] fix: address CodeRabbit review issues - Remove duplicate teamExists call in signup screen - Fix security issues by not exposing raw exceptions in error messages - Fix duplicate timer in splash screen - Avoid mutating unmodifiable map by copying arguments --- lib/screens/auth/login_screen.dart | 8 ++++---- lib/screens/auth/signup_screen.dart | 19 +++++++++++++++++-- lib/screens/splash_screen.dart | 8 ++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index 8e39b68..74b4fbd 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -111,11 +111,11 @@ class _LoginScreenState extends State } } } catch (e) { - print('[LoginScreen] Login error: $e'); + print('[LoginScreen] Login error: $e'); // Log for debugging if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Login failed: ${e.toString()}'), + const SnackBar( + content: Text('Login failed. Please check your credentials and try again.'), backgroundColor: Colors.red, ), ); @@ -225,4 +225,4 @@ class _LoginScreenState extends State ], ); } -} +} \ No newline at end of file diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index 91a1929..c73615f 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -80,9 +80,13 @@ class _SignupScreenState extends State with SingleTickerProviderSt ); } } catch (e) { + print('[SignupScreen] Error sending OTP: $e'); // Log for debugging if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString()), backgroundColor: Colors.red), + const SnackBar( + content: Text('Failed to send verification email. Please try again.'), + backgroundColor: Colors.red, + ), ); } } finally { @@ -108,6 +112,7 @@ class _SignupScreenState extends State with SingleTickerProviderSt setState(() => _isLoading = true); try { + // Check if the team exists final teamExists = await _supabaseService.teamExists(_teamIdController.text); if (!teamExists) { @@ -130,6 +135,16 @@ class _SignupScreenState extends State with SingleTickerProviderSt }; await _sendOtpAndNavigate('signup_join', userData); + } catch (e) { + print('[SignupScreen] Error checking team: $e'); // Log for debugging + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to verify team. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } } finally { if (mounted) setState(() => _isLoading = false); } @@ -307,4 +322,4 @@ class _SignupScreenState extends State with SingleTickerProviderSt ], ); } -} +} \ No newline at end of file diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 5b4872f..be93341 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -46,7 +46,7 @@ class _SplashScreenState extends State _animationController.forward(); // Give a small delay to ensure everything is initialized - Timer(const Duration(seconds: 3), _checkSession); + Timer(const Duration(milliseconds: 1500), _checkSession); } Future _checkSession() async { @@ -61,8 +61,8 @@ class _SplashScreenState extends State int? screenIndex = _getScreenIndex(initialShortcut); debugPrint('[SplashScreen] Converted screen index: $screenIndex'); - // Merge with any widget arguments - Map homeScreenArgs = widget.arguments ?? {}; + // Create a copy of arguments to avoid mutating potentially unmodifiable map + Map homeScreenArgs = Map.from(widget.arguments ?? {}); if (screenIndex != null) { homeScreenArgs['screen'] = screenIndex; homeScreenArgs['initial_route'] = initialShortcut; @@ -211,4 +211,4 @@ class _SplashScreenState extends State ), ); } -} +} \ No newline at end of file