diff --git a/app/lib/presentation/navigation/routers.dart b/app/lib/presentation/navigation/routers.dart index cde504c..44bd1a0 100644 --- a/app/lib/presentation/navigation/routers.dart +++ b/app/lib/presentation/navigation/routers.dart @@ -3,6 +3,7 @@ import 'package:app/presentation/ui/custom/debug_banner.dart'; import 'package:app/presentation/ui/pages/main/home/home_page.dart'; import 'package:app/presentation/ui/pages/auth/login/login_page.dart'; import 'package:app/presentation/ui/pages/auth/sign_up/sign_up_page.dart'; +import 'package:app/presentation/ui/pages/onboarding/onboarding_page.dart'; import 'package:app/presentation/ui/pages/splash/splash_page.dart'; import 'package:common/core/resource.dart'; import 'package:domain/bloc/auth/auth_cubit.dart'; @@ -14,6 +15,7 @@ import 'package:go_router/go_router.dart'; enum Routes { auth, + onboarding, login, signup, app, @@ -50,7 +52,7 @@ class Routers { initialLocation: initialLocation ?? (getIt().isLoggedIn() ? Routes.app.path - : Routes.auth.path), + : Routes.onboarding.path), routes: [ GoRoute( path: '/', @@ -79,7 +81,7 @@ class Routers { return; } debugPrint('Navigating to auth route'); - Routes.auth.go(context); + Routes.onboarding.go(context); break; case _: } @@ -93,6 +95,11 @@ class Routers { builder: (context, state, child) => kDebugMode ? DebugBanner(child: child) : child, routes: [ + GoRoute( + name: Routes.onboarding.name, + path: Routes.onboarding.path, + builder: (context, state) => const OnboardingPage(), + ), GoRoute( name: Routes.auth.name, path: Routes.auth.path, diff --git a/app/lib/presentation/resources/dim.dart b/app/lib/presentation/resources/dim.dart index 027ea22..ed1f904 100644 --- a/app/lib/presentation/resources/dim.dart +++ b/app/lib/presentation/resources/dim.dart @@ -19,4 +19,5 @@ class Dimen { static const spacingXxxl = 48.0; static const double buttonHeightM = 48.0; + static const double onboardingIconSize = 120.0; } diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart index e99d512..f7ead05 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart @@ -20,6 +20,9 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; + static String m0(currentPage, totalPages) => + "Page ${currentPage} of ${totalPages}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), @@ -70,6 +73,26 @@ class MessageLookup extends MessageLookupByLibrary { "Invalid email or password.", ), "noConnection": MessageLookupByLibrary.simpleMessage("No connection"), + "onboardingBack": MessageLookupByLibrary.simpleMessage("Back"), + "onboardingNext": MessageLookupByLibrary.simpleMessage("Next"), + "onboardingPage1Description": MessageLookupByLibrary.simpleMessage( + "This is the first page of the onboarding flow", + ), + "onboardingPage1Title": MessageLookupByLibrary.simpleMessage("Welcome"), + "onboardingPage2Description": MessageLookupByLibrary.simpleMessage( + "This is the second page of the onboarding flow", + ), + "onboardingPage2Title": MessageLookupByLibrary.simpleMessage("Discover"), + "onboardingPage3Description": MessageLookupByLibrary.simpleMessage( + "This is the third page of the onboarding flow", + ), + "onboardingPage3Title": MessageLookupByLibrary.simpleMessage("Connect"), + "onboardingPage4Description": MessageLookupByLibrary.simpleMessage( + "This is the fourth page of the onboarding flow", + ), + "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Get Started"), + "onboardingPageIndicator": m0, + "onboardingStart": MessageLookupByLibrary.simpleMessage("Start"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.", ), diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart index 76be69b..6d8e904 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart @@ -20,6 +20,9 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'es'; + static String m0(currentPage, totalPages) => + "Página ${currentPage} de ${totalPages}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), @@ -72,6 +75,26 @@ class MessageLookup extends MessageLookupByLibrary { "Correo electrónico o contraseña incorrectos.", ), "noConnection": MessageLookupByLibrary.simpleMessage("Sin conexión"), + "onboardingBack": MessageLookupByLibrary.simpleMessage("Atrás"), + "onboardingNext": MessageLookupByLibrary.simpleMessage("Siguiente"), + "onboardingPage1Description": MessageLookupByLibrary.simpleMessage( + "Esta es la primera página del flujo de incorporación", + ), + "onboardingPage1Title": MessageLookupByLibrary.simpleMessage("Bienvenido"), + "onboardingPage2Description": MessageLookupByLibrary.simpleMessage( + "Esta es la segunda página del flujo de incorporación", + ), + "onboardingPage2Title": MessageLookupByLibrary.simpleMessage("Descubrir"), + "onboardingPage3Description": MessageLookupByLibrary.simpleMessage( + "Esta es la tercera página del flujo de incorporación", + ), + "onboardingPage3Title": MessageLookupByLibrary.simpleMessage("Conectar"), + "onboardingPage4Description": MessageLookupByLibrary.simpleMessage( + "Esta es la cuarta página del flujo de incorporación", + ), + "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Comenzar"), + "onboardingPageIndicator": m0, + "onboardingStart": MessageLookupByLibrary.simpleMessage("Comenzar"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Mínimo 8 caracteres: 1 mayúscula, 1 minúscula, 1 número y 1 carácter especial.", ), diff --git a/app/lib/presentation/resources/locale/generated/l10n.dart b/app/lib/presentation/resources/locale/generated/l10n.dart index c6fbafc..d3696f8 100644 --- a/app/lib/presentation/resources/locale/generated/l10n.dart +++ b/app/lib/presentation/resources/locale/generated/l10n.dart @@ -314,6 +314,111 @@ class S { args: [], ); } + + /// `Back` + String get onboardingBack { + return Intl.message('Back', name: 'onboardingBack', desc: '', args: []); + } + + /// `Next` + String get onboardingNext { + return Intl.message('Next', name: 'onboardingNext', desc: '', args: []); + } + + /// `Start` + String get onboardingStart { + return Intl.message('Start', name: 'onboardingStart', desc: '', args: []); + } + + /// `Welcome` + String get onboardingPage1Title { + return Intl.message( + 'Welcome', + name: 'onboardingPage1Title', + desc: '', + args: [], + ); + } + + /// `This is the first page of the onboarding flow` + String get onboardingPage1Description { + return Intl.message( + 'This is the first page of the onboarding flow', + name: 'onboardingPage1Description', + desc: '', + args: [], + ); + } + + /// `Discover` + String get onboardingPage2Title { + return Intl.message( + 'Discover', + name: 'onboardingPage2Title', + desc: '', + args: [], + ); + } + + /// `This is the second page of the onboarding flow` + String get onboardingPage2Description { + return Intl.message( + 'This is the second page of the onboarding flow', + name: 'onboardingPage2Description', + desc: '', + args: [], + ); + } + + /// `Connect` + String get onboardingPage3Title { + return Intl.message( + 'Connect', + name: 'onboardingPage3Title', + desc: '', + args: [], + ); + } + + /// `This is the third page of the onboarding flow` + String get onboardingPage3Description { + return Intl.message( + 'This is the third page of the onboarding flow', + name: 'onboardingPage3Description', + desc: '', + args: [], + ); + } + + /// `Get Started` + String get onboardingPage4Title { + return Intl.message( + 'Get Started', + name: 'onboardingPage4Title', + desc: '', + args: [], + ); + } + + /// `This is the fourth page of the onboarding flow` + String get onboardingPage4Description { + return Intl.message( + 'This is the fourth page of the onboarding flow', + name: 'onboardingPage4Description', + desc: '', + args: [], + ); + } + + /// `Page {currentPage} of {totalPages}` + String onboardingPageIndicator(Object currentPage, Object totalPages) { + return Intl.message( + 'Page $currentPage of $totalPages', + name: 'onboardingPageIndicator', + desc: '', + args: [currentPage, totalPages], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/app/lib/presentation/resources/locale/intl_en.arb b/app/lib/presentation/resources/locale/intl_en.arb index f0b4445..0a541e4 100644 --- a/app/lib/presentation/resources/locale/intl_en.arb +++ b/app/lib/presentation/resources/locale/intl_en.arb @@ -30,5 +30,17 @@ "debugModeResetAppTitle": "Reset App", "debugModeResetAppMessage": "Are you sure you want to reset the app?", "debugModeCancel": "Cancel", - "debugModeConfirm": "Confirm" -} + "debugModeConfirm": "Confirm", + "onboardingBack": "Back", + "onboardingNext": "Next", + "onboardingStart": "Start", + "onboardingPage1Title": "Welcome", + "onboardingPage1Description": "This is the first page of the onboarding flow", + "onboardingPage2Title": "Discover", + "onboardingPage2Description": "This is the second page of the onboarding flow", + "onboardingPage3Title": "Connect", + "onboardingPage3Description": "This is the third page of the onboarding flow", + "onboardingPage4Title": "Get Started", + "onboardingPage4Description": "This is the fourth page of the onboarding flow", + "onboardingPageIndicator": "Page {currentPage} of {totalPages}" +} \ No newline at end of file diff --git a/app/lib/presentation/resources/locale/intl_es.arb b/app/lib/presentation/resources/locale/intl_es.arb index 9a95d34..cd3eea8 100644 --- a/app/lib/presentation/resources/locale/intl_es.arb +++ b/app/lib/presentation/resources/locale/intl_es.arb @@ -30,5 +30,17 @@ "debugModeResetAppTitle": "Resetear App", "debugModeResetAppMessage": "¿Estás seguro de que deseas resetear la aplicación?", "debugModeCancel": "Cancelar", - "debugModeConfirm": "Confirmar" + "debugModeConfirm": "Confirmar", + "onboardingBack": "Atrás", + "onboardingNext": "Siguiente", + "onboardingStart": "Comenzar", + "onboardingPage1Title": "Bienvenido", + "onboardingPage1Description": "Esta es la primera página del flujo de incorporación", + "onboardingPage2Title": "Descubrir", + "onboardingPage2Description": "Esta es la segunda página del flujo de incorporación", + "onboardingPage3Title": "Conectar", + "onboardingPage3Description": "Esta es la tercera página del flujo de incorporación", + "onboardingPage4Title": "Comenzar", + "onboardingPage4Description": "Esta es la cuarta página del flujo de incorporación", + "onboardingPageIndicator": "Página {currentPage} de {totalPages}" } \ No newline at end of file diff --git a/app/lib/presentation/ui/pages/auth/login/login_form.dart b/app/lib/presentation/ui/pages/auth/login/login_form.dart index 0eb85f6..25be1bd 100644 --- a/app/lib/presentation/ui/pages/auth/login/login_form.dart +++ b/app/lib/presentation/ui/pages/auth/login/login_form.dart @@ -72,18 +72,13 @@ class _LoginFormState extends State { obscureText: true, controller: passwordController, validator: (value) { - if (!FormValidator.isStrongPassword(value)) { - return S.of(context).errorPasswordWeak; + if (value?.trim().isEmpty ?? true) { + return S.of(context).errorPasswordRequired; } return null; }, ), const Gap(Dimen.spacingM), - Text( - S.of(context).passwordInstructions, - style: Theme.of(context).textTheme.bodySmall, - ), - const Gap(Dimen.spacingM), TermsServicesCheck( agreeToTerms: agreeToTerms, onChanged: (value) { diff --git a/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart new file mode 100644 index 0000000..cdb131b --- /dev/null +++ b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart @@ -0,0 +1,212 @@ +import 'package:app/presentation/navigation/routers.dart'; +import 'package:app/presentation/resources/locale/generated/l10n.dart'; +import 'package:app/presentation/resources/resources.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + final PageController _pageController = PageController(); + final FocusNode _focusNode = FocusNode(); + int _currentPage = 0; + final int _totalPages = 4; + + @override + void dispose() { + _pageController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _onPageChanged(int page) { + setState(() { + _currentPage = page; + }); + } + + void _nextPage() { + if (_currentPage < _totalPages - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.bounceInOut, + ); + } else { + // On the last page, handle start action + _onStart(); + } + } + + void _previousPage() { + if (_currentPage > 0) { + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _onStart() { + Routes.auth.go(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Focus( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + _nextPage(); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _previousPage(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: SafeArea( + child: Column( + children: [ + // Page Indicator + Padding( + padding: const EdgeInsets.all(Dimen.spacingM), + child: Semantics( + label: S.of(context).onboardingPageIndicator( + _currentPage + 1, + _totalPages, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _totalPages, + (index) => Container( + margin: const EdgeInsets.symmetric( + horizontal: Dimen.spacingXs), + width: _currentPage == index + ? Dimen.spacingL + : Dimen.spacingS, + height: Dimen.spacingS, + decoration: BoxDecoration( + color: _currentPage == index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + borderRadius: BorderRadius.circular(Dimen.spacingXs), + ), + ), + ), + ), + ), + ), + // PageView + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: _onPageChanged, + children: [ + _buildPage( + title: S.of(context).onboardingPage1Title, + description: S.of(context).onboardingPage1Description, + icon: Icons.waving_hand, + color: Theme.of(context).colorScheme.primary, + ), + _buildPage( + title: S.of(context).onboardingPage2Title, + description: S.of(context).onboardingPage2Description, + icon: Icons.explore, + color: Theme.of(context).colorScheme.secondary, + ), + _buildPage( + title: S.of(context).onboardingPage3Title, + description: S.of(context).onboardingPage3Description, + icon: Icons.people, + color: Theme.of(context).colorScheme.tertiary, + ), + _buildPage( + title: S.of(context).onboardingPage4Title, + description: S.of(context).onboardingPage4Description, + icon: Icons.rocket_launch, + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ), + // Navigation Buttons + Padding( + padding: const EdgeInsets.all(Dimen.spacingL), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Back Button (hidden on first page) + if (_currentPage > 0) + TextButton( + onPressed: _previousPage, + child: Text(S.of(context).onboardingBack), + ) + else + const SizedBox(width: Dimen.spacingL), + // Next/Start Button + ElevatedButton( + onPressed: _nextPage, + child: Text( + _currentPage == _totalPages - 1 + ? S.of(context).onboardingStart + : S.of(context).onboardingNext, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPage({ + required String title, + required String description, + required IconData icon, + required Color color, + }) { + return Padding( + padding: const EdgeInsets.all(Dimen.spacingXl), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: Dimen.onboardingIconSize, + color: color, + ), + const SizedBox(height: Dimen.spacingXl), + Text( + title, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: Dimen.spacingM), + Text( + description, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/modules/domain/devtools_options.yaml b/modules/domain/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/modules/domain/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: