From 2dcb4b4f679647c165e815c8f6cb914cf6206f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Mon, 16 Feb 2026 04:23:00 +0100 Subject: [PATCH] Small Flutter UI tweaks --- mobile/lib/main.dart | 12 + mobile/lib/screens/chat_list_screen.dart | 17 +- mobile/lib/screens/dashboard_screen.dart | 1 - mobile/lib/screens/intro_screen.dart | 13 + mobile/lib/screens/intro_screen_stub.dart | 39 +++ mobile/lib/screens/intro_screen_web.dart | 242 ++++++++++++++++++ .../lib/screens/main_navigation_screen.dart | 144 ++++++++--- mobile/lib/screens/more_screen.dart | 3 - 8 files changed, 432 insertions(+), 39 deletions(-) create mode 100644 mobile/lib/screens/intro_screen.dart create mode 100644 mobile/lib/screens/intro_screen_stub.dart create mode 100644 mobile/lib/screens/intro_screen_web.dart diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4c7489b3b..b00bbe480 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -65,6 +65,12 @@ class SureApp extends StatelessWidget { title: 'Sure Finance', debugShowCheckedModeBanner: false, theme: ThemeData( + fontFamily: 'Geist', + fontFamilyFallback: const [ + 'Inter', + 'Arial', + 'sans-serif', + ], colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF6366F1), brightness: Brightness.light, @@ -96,6 +102,12 @@ class SureApp extends StatelessWidget { ), ), darkTheme: ThemeData( + fontFamily: 'Geist', + fontFamilyFallback: const [ + 'Inter', + 'Arial', + 'sans-serif', + ], colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF6366F1), brightness: Brightness.dark, diff --git a/mobile/lib/screens/chat_list_screen.dart b/mobile/lib/screens/chat_list_screen.dart index 1039b3309..eece17448 100644 --- a/mobile/lib/screens/chat_list_screen.dart +++ b/mobile/lib/screens/chat_list_screen.dart @@ -110,12 +110,19 @@ class _ChatListScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('AI Assistant'), + title: const Text('Chats'), + centerTitle: false, actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _handleRefresh, - tooltip: 'Refresh', + Padding( + padding: const EdgeInsets.only(top: 12, right: 12), + child: InkWell( + onTap: _handleRefresh, + child: const SizedBox( + width: 36, + height: 36, + child: Icon(Icons.refresh), + ), + ), ), ], ), diff --git a/mobile/lib/screens/dashboard_screen.dart b/mobile/lib/screens/dashboard_screen.dart index 2ab6c6d68..9b872b7ab 100644 --- a/mobile/lib/screens/dashboard_screen.dart +++ b/mobile/lib/screens/dashboard_screen.dart @@ -373,7 +373,6 @@ class DashboardScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('Dashboard'), actions: [ if (_showSyncSuccess) Padding( diff --git a/mobile/lib/screens/intro_screen.dart b/mobile/lib/screens/intro_screen.dart new file mode 100644 index 000000000..82a323135 --- /dev/null +++ b/mobile/lib/screens/intro_screen.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'intro_screen_stub.dart' if (dart.library.html) 'intro_screen_web.dart'; + +class IntroScreen extends StatelessWidget { + const IntroScreen({super.key, this.onStartChat}); + + final VoidCallback? onStartChat; + + @override + Widget build(BuildContext context) { + return IntroScreenPlatform(onStartChat: onStartChat); + } +} diff --git a/mobile/lib/screens/intro_screen_stub.dart b/mobile/lib/screens/intro_screen_stub.dart new file mode 100644 index 000000000..c0224c0c7 --- /dev/null +++ b/mobile/lib/screens/intro_screen_stub.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class IntroScreenPlatform extends StatelessWidget { + const IntroScreenPlatform({super.key, this.onStartChat}); + + final VoidCallback? onStartChat; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: const Card( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Text( + 'Intro experience coming soon', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + SizedBox(height: 12), + Text( + "We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. " + 'For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.', + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/screens/intro_screen_web.dart b/mobile/lib/screens/intro_screen_web.dart new file mode 100644 index 000000000..ecec9a131 --- /dev/null +++ b/mobile/lib/screens/intro_screen_web.dart @@ -0,0 +1,242 @@ +import 'dart:html' as html; +import 'dart:ui_web' as ui; +import 'package:flutter/material.dart'; + +class IntroScreenPlatform extends StatefulWidget { + const IntroScreenPlatform({super.key, this.onStartChat}); + + final VoidCallback? onStartChat; + + @override + State createState() => _IntroScreenPlatformState(); +} + +class _IntroScreenPlatformState extends State { + static int _nextViewId = 0; + + late final String _viewType; + + @override + void initState() { + super.initState(); + final currentId = _nextViewId; + _nextViewId += 1; + _viewType = 'intro-screen-web-$currentId'; + + ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { + final frame = html.IFrameElement() + ..srcdoc = _introHtmlContent + ..style.width = '100%' + ..style.height = '100%' + ..style.border = '0'; + + return frame; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SizedBox.expand( + child: HtmlElementView(viewType: _viewType), + ), + ); + } +} + +const String _introHtmlContent = ''' + + + + + + + + +
+
+
+

Intro experience coming soon

+

+ We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey. +

+
+
+
+ + +'''; diff --git a/mobile/lib/screens/main_navigation_screen.dart b/mobile/lib/screens/main_navigation_screen.dart index 7a5a05f14..3c80f52ae 100644 --- a/mobile/lib/screens/main_navigation_screen.dart +++ b/mobile/lib/screens/main_navigation_screen.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; + import '../providers/auth_provider.dart'; -import 'dashboard_screen.dart'; import 'chat_list_screen.dart'; +import 'dashboard_screen.dart'; +import 'intro_screen.dart'; import 'more_screen.dart'; import 'settings_screen.dart'; @@ -17,13 +20,17 @@ class _MainNavigationScreenState extends State { int _currentIndex = 0; final _dashboardKey = GlobalKey(); - List _buildScreens(bool introLayout) { + List _buildScreens(bool introLayout, VoidCallback? onStartChat) { final screens = []; if (!introLayout) { screens.add(DashboardScreen(key: _dashboardKey)); } + if (introLayout) { + screens.add(IntroScreen(onStartChat: onStartChat)); + } + screens.add(const ChatListScreen()); if (!introLayout) { @@ -35,6 +42,36 @@ class _MainNavigationScreenState extends State { return screens; } + Future _handleDestinationSelected( + int index, + AuthProvider authProvider, + bool introLayout, + ) async { + const chatIndex = 1; + + if (index == chatIndex && !authProvider.aiEnabled) { + final enabled = await _showEnableAiPrompt(); + if (!enabled) { + return; + } + } + + if (mounted) { + setState(() { + _currentIndex = index; + }); + + if (!introLayout && index == 0) { + _dashboardKey.currentState?.reloadPreferences(); + } + } + } + + Future _handleSelectSettings(AuthProvider authProvider, bool introLayout) async { + final settingsIndex = introLayout ? 2 : 3; + await _handleDestinationSelected(settingsIndex, authProvider, introLayout); + } + List _buildDestinations(bool introLayout) { final destinations = []; @@ -48,6 +85,16 @@ class _MainNavigationScreenState extends State { ); } + if (introLayout) { + destinations.add( + const NavigationDestination( + icon: Icon(Icons.auto_awesome_outlined), + selectedIcon: Icon(Icons.auto_awesome), + label: 'Intro', + ), + ); + } + destinations.add( const NavigationDestination( icon: Icon(Icons.chat_bubble_outline), @@ -66,17 +113,48 @@ class _MainNavigationScreenState extends State { ); } - destinations.add( - const NavigationDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings), - label: 'Settings', - ), - ); - return destinations; } + PreferredSizeWidget _buildTopBar(AuthProvider authProvider, bool introLayout) { + return AppBar( + automaticallyImplyLeading: false, + toolbarHeight: 60, + elevation: 0, + titleSpacing: 0, + centerTitle: false, + actionsPadding: EdgeInsets.zero, + title: Container( + width: 60, + height: 60, + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 12), + child: SvgPicture.asset( + 'assets/images/logomark.svg', + width: 36, + height: 36, + ), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(top: 12, right: 12), + child: InkWell( + onTap: () { + _handleSelectSettings(authProvider, introLayout); + }, + child: const SizedBox( + width: 36, + height: 36, + child: Icon(Icons.settings_outlined), + ), + ), + ), + ], + ); + } + Future _showEnableAiPrompt() async { final authProvider = Provider.of(context, listen: false); @@ -116,43 +194,49 @@ class _MainNavigationScreenState extends State { return enabled; } + int _resolveBottomSelectedIndex(List destinations) { + if (destinations.isEmpty) { + return 0; + } + + if (_currentIndex < 0) { + return 0; + } + + if (_currentIndex >= destinations.length) { + return destinations.length - 1; + } + + return _currentIndex; + } + @override Widget build(BuildContext context) { return Consumer( builder: (context, authProvider, _) { final introLayout = authProvider.isIntroLayout; - final screens = _buildScreens(introLayout); + const chatIndex = 1; + final screens = _buildScreens( + introLayout, + () => _handleDestinationSelected(chatIndex, authProvider, introLayout), + ); final destinations = _buildDestinations(introLayout); + final bottomNavIndex = _resolveBottomSelectedIndex(destinations); if (_currentIndex >= screens.length) { _currentIndex = 0; } - final chatIndex = introLayout ? 0 : 1; - final homeIndex = 0; - return Scaffold( + appBar: _buildTopBar(authProvider, introLayout), body: IndexedStack( index: _currentIndex, children: screens, ), bottomNavigationBar: NavigationBar( - selectedIndex: _currentIndex, - onDestinationSelected: (index) async { - if (index == chatIndex && !authProvider.aiEnabled) { - final enabled = await _showEnableAiPrompt(); - if (!enabled) { - return; - } - } - - setState(() { - _currentIndex = index; - }); - - if (!introLayout && index == homeIndex) { - _dashboardKey.currentState?.reloadPreferences(); - } + selectedIndex: bottomNavIndex, + onDestinationSelected: (index) { + _handleDestinationSelected(index, authProvider, introLayout); }, destinations: destinations, ), diff --git a/mobile/lib/screens/more_screen.dart b/mobile/lib/screens/more_screen.dart index 80cf4ba4a..970b20eab 100644 --- a/mobile/lib/screens/more_screen.dart +++ b/mobile/lib/screens/more_screen.dart @@ -10,9 +10,6 @@ class MoreScreen extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return Scaffold( - appBar: AppBar( - title: const Text('More'), - ), body: ListView( children: [ _buildMenuItem(