From 791d71d7cda9209064da55ead69ef8976ebabb8a Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 12 Nov 2025 14:42:44 -0300 Subject: [PATCH 1/3] refactor auth service and auth cubit logic --- .../presentation/ui/pages/home/home_view.dart | 8 +- .../ui/pages/login/login_page.dart | 9 +- .../ui/pages/splash/splash_page.dart | 13 ++- app/melos.yaml | 3 + modules/domain/lib/bloc/auth/auth_cubit.dart | 32 ++++++- modules/domain/lib/init.dart | 10 +-- modules/domain/lib/services/auth_service.dart | 26 ++---- modules/domain/test/auth_cubit_test.dart | 48 ----------- modules/domain/test/auth_service_test.dart | 86 ------------------- 9 files changed, 63 insertions(+), 172 deletions(-) delete mode 100644 modules/domain/test/auth_cubit_test.dart delete mode 100644 modules/domain/test/auth_service_test.dart diff --git a/app/lib/presentation/ui/pages/home/home_view.dart b/app/lib/presentation/ui/pages/home/home_view.dart index d3ed9d9..07d473e 100644 --- a/app/lib/presentation/ui/pages/home/home_view.dart +++ b/app/lib/presentation/ui/pages/home/home_view.dart @@ -1,10 +1,12 @@ import 'package:app/main/init.dart'; -import 'package:domain/services/auth_service.dart'; +import 'package:domain/bloc/auth/auth_cubit.dart'; import 'package:flutter/material.dart'; import 'package:app/presentation/ui/custom/app_theme_switch.dart'; class HomeView extends StatelessWidget { - AuthService get _authService => getIt(); + /// Given this is a global cubit, we can access it directly from getIt + /// other wise use context.read() to read the Cubit under that context + AuthCubit get _authCubit => getIt(); const HomeView({super.key}); @@ -14,7 +16,7 @@ class HomeView extends StatelessWidget { appBar: AppBar( actions: [ IconButton( - onPressed: () => _authService.onLogout(), + onPressed: () => _authCubit.logOut(), icon: const Icon(Icons.logout), ), const AppThemeSwitch(), diff --git a/app/lib/presentation/ui/pages/login/login_page.dart b/app/lib/presentation/ui/pages/login/login_page.dart index 17777c6..d97c3b6 100644 --- a/app/lib/presentation/ui/pages/login/login_page.dart +++ b/app/lib/presentation/ui/pages/login/login_page.dart @@ -5,7 +5,6 @@ import 'package:app/presentation/ui/custom/app_theme_switch.dart'; import 'package:app/presentation/ui/custom/loading_screen.dart'; import 'package:common/core/resource.dart'; import 'package:domain/bloc/auth/auth_cubit.dart'; -import 'package:domain/services/auth_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,10 +12,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../custom/environment_selector.dart'; class LoginPage extends StatelessWidget { - AuthService get _authService => getIt(); - const LoginPage({super.key}); + AuthCubit get _authCubit => getIt(); + @override Widget build(BuildContext context) { return Stack( @@ -29,6 +28,7 @@ class LoginPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + const _Loading(), const AppThemeSwitch(), SizedBox(height: spacing.m), SizedBox( @@ -36,7 +36,7 @@ class LoginPage extends StatelessWidget { child: ElevatedButton( child: const Text('Login'), onPressed: () { - _authService.logInWithCredentials( + _authCubit.login( 'Rootstrap', '12345678', ); @@ -51,7 +51,6 @@ class LoginPage extends StatelessWidget { ), ), ), - const _Loading(), ], ); } diff --git a/app/lib/presentation/ui/pages/splash/splash_page.dart b/app/lib/presentation/ui/pages/splash/splash_page.dart index a43fc5a..6f9bb4c 100644 --- a/app/lib/presentation/ui/pages/splash/splash_page.dart +++ b/app/lib/presentation/ui/pages/splash/splash_page.dart @@ -1,5 +1,5 @@ import 'package:app/main/init.dart'; -import 'package:domain/services/auth_service.dart'; +import 'package:domain/bloc/auth/auth_cubit.dart'; import 'package:flutter/material.dart'; class SplashPage extends StatefulWidget { @@ -10,12 +10,19 @@ class SplashPage extends StatefulWidget { } class _SplashPageState extends State { - AuthService get _authService => getIt(); + /// Given this is a global cubit, we can access it directly from getIt + /// other wise use context.read() to read the Cubit under that context + AuthCubit get _authCubit => getIt(); @override void initState() { super.initState(); - _authService.onValidate(); + + /// Add post frame callback to avoid calling bloc methods during build + WidgetsBinding.instance.addPostFrameCallback((_) async { + await Future.delayed(const Duration(seconds: 1)); + _authCubit.onValidate(); + }); } @override diff --git a/app/melos.yaml b/app/melos.yaml index 0923c00..790b116 100644 --- a/app/melos.yaml +++ b/app/melos.yaml @@ -5,6 +5,9 @@ packages: - modules/* scripts: + run:web: + exec: cd app && flutter run -t lib/main.dart --dart-define-from-file=env/.dev -d chrome + description: Run the app in development mode for web. lint:all: run: melos run analyze && melos run format description: Run all static analysis checks. diff --git a/modules/domain/lib/bloc/auth/auth_cubit.dart b/modules/domain/lib/bloc/auth/auth_cubit.dart index 99f0f0d..8c84600 100644 --- a/modules/domain/lib/bloc/auth/auth_cubit.dart +++ b/modules/domain/lib/bloc/auth/auth_cubit.dart @@ -1,9 +1,39 @@ import 'package:common/core/resource.dart'; +import 'package:common/core/result_type.dart'; import 'package:domain/bloc/base_cubit.dart'; import 'package:domain/bloc/auth/auth_state.dart'; +import 'package:domain/services/auth_service.dart'; class AuthCubit extends BaseCubit { - AuthCubit() : super(RSuccess(data: AuthStateUnknown())); + final AuthService _authService; + AuthCubit(this._authService) : super(RSuccess(data: AuthStateUnknown())); + + Future login(String username, String password) async { + isLoading(); + final authResult = + await _authService.logInWithCredentials(username, password); + + authResult.map( + success: (_) => isLogin(), + error: (failure) { + isError(failure); + return failure; + }, + ); + } + + Future onValidate() async { + if (_authService.isLoggedIn()) { + isLogin(); + } else { + isLogOut(); + } + } + + Future logOut() async { + await _authService.onLogout(); + isLogOut(); + } void isLogin() => isSuccess(AuthStateAuthenticated()); diff --git a/modules/domain/lib/init.dart b/modules/domain/lib/init.dart index 2b50a02..323ee9f 100644 --- a/modules/domain/lib/init.dart +++ b/modules/domain/lib/init.dart @@ -5,11 +5,11 @@ import 'package:get_it/get_it.dart'; class DomainInit { static Future initialize(GetIt getIt) async { - //Cubits + //Services (with dependencies to repositories) + getIt.registerLazySingleton(() => AuthService(getIt())); + + //Global Cubits getIt.registerSingleton(AppCubit(getIt())); - getIt.registerSingleton(AuthCubit()); - - //Services - getIt.registerLazySingleton(() => AuthService(getIt(), getIt())); + getIt.registerSingleton(AuthCubit(getIt())); } } diff --git a/modules/domain/lib/services/auth_service.dart b/modules/domain/lib/services/auth_service.dart index 938828a..0327d2a 100644 --- a/modules/domain/lib/services/auth_service.dart +++ b/modules/domain/lib/services/auth_service.dart @@ -1,34 +1,18 @@ import 'package:common/core/result_type.dart'; -import 'package:domain/bloc/auth/auth_cubit.dart'; import 'package:domain/repositories/auth_repository.dart'; class AuthService { final AuthRepository _authRepository; - final AuthCubit _sessionCubit; - AuthService(this._authRepository, this._sessionCubit); + AuthService(this._authRepository); - Future logInWithCredentials(String username, String password) async { - _sessionCubit.isLoading(); - final result = await _authRepository.login(username, password); - switch (result) { - case TSuccess _: - _sessionCubit.isLogin(); - case TError _: - _sessionCubit.isError(result.error); - } - } + Future> logInWithCredentials( + String username, String password) => + _authRepository.login(username, password); - void onValidate() { - if (_authRepository.isLoggedIn()) { - _sessionCubit.isLogin(); - } else { - _sessionCubit.isLogOut(); - } - } + bool isLoggedIn() => _authRepository.isLoggedIn(); Future onLogout() async { await _authRepository.logout(); - _sessionCubit.isLogOut(); } } diff --git a/modules/domain/test/auth_cubit_test.dart b/modules/domain/test/auth_cubit_test.dart deleted file mode 100644 index bf62fd8..0000000 --- a/modules/domain/test/auth_cubit_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:common/core/resource.dart'; -import 'package:domain/bloc/auth/auth_cubit.dart'; -import 'package:domain/bloc/auth/auth_state.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('AuthCubit', () { - late AuthCubit cubit; - - setUp(() { - cubit = AuthCubit(); - }); - - tearDown(() { - cubit.close(); - }); - - test('emits AuthStateAuthenticated when isLogin is called', () { - cubit.isLogin(); - - expect(cubit.state, isA>()); - - final successState = cubit.state as RSuccess; - - expect(successState.data, isA()); - }); - - test('emits AuthStateUnauthenticated when isLogOut is called', () { - cubit.isLogOut(); - - expect(cubit.state, isA>()); - - final successState = cubit.state as RSuccess; - - expect(successState.data, isA()); - }); - - test('emits AuthStateUnknown when isUnknown is called', () { - cubit.isUnknown(); - - expect(cubit.state, isA>()); - - final successState = cubit.state as RSuccess; - - expect(successState.data, isA()); - }); - }); -} diff --git a/modules/domain/test/auth_service_test.dart b/modules/domain/test/auth_service_test.dart deleted file mode 100644 index 61d1796..0000000 --- a/modules/domain/test/auth_service_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:common/core/result_type.dart'; -import 'package:domain/bloc/auth/auth_cubit.dart'; -import 'package:domain/repositories/auth_repository.dart'; -import 'package:domain/services/auth_service.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAuthRepository extends Mock implements AuthRepository {} - -class MockSessionCubit extends Mock implements AuthCubit {} - -void main() { - late AuthRepository mockAuthRepository; - late AuthCubit mockSessionCubit; - late AuthService authService; - - const username = 'testuser'; - const password = 'password123'; - final failure = Exception('Invalid credentials'); - - setUp(() { - mockAuthRepository = MockAuthRepository(); - mockSessionCubit = MockSessionCubit(); - authService = AuthService(mockAuthRepository, mockSessionCubit); - }); - - group('AuthService', () { - test('logInWithCredentials calls isLogin on success', () async { - when(() => mockAuthRepository.login(username, password)) - .thenAnswer((_) async => TSuccess(null)); - when(() => mockSessionCubit.isLoading()).thenReturn(null); - when(() => mockSessionCubit.isLogin()).thenReturn(null); - - await authService.logInWithCredentials(username, password); - - verify(() => mockSessionCubit.isLoading()).called(1); - verify(() => mockSessionCubit.isLogin()).called(1); - verifyNever(() => mockSessionCubit.isError(any())); - }); - - test('logInWithCredentials calls isError on failure', () async { - when(() => mockAuthRepository.login(username, password)) - .thenAnswer((_) async => TError(failure)); - when(() => mockSessionCubit.isLoading()).thenReturn(null); - when(() => mockSessionCubit.isError(failure)).thenReturn(null); - - await authService.logInWithCredentials(username, password); - - verify(() => mockSessionCubit.isLoading()).called(1); - verify(() => mockSessionCubit.isError(failure)).called(1); - verifyNever(() => mockSessionCubit.isLogin()); - }); - - test('onValidate calls isLogin when user is logged in', () { - when(() => mockAuthRepository.isLoggedIn()).thenReturn(true); - when(() => mockSessionCubit.isLogin()).thenReturn(null); - - authService.onValidate(); - - verify(() => mockAuthRepository.isLoggedIn()).called(1); - verify(() => mockSessionCubit.isLogin()).called(1); - verifyNever(() => mockSessionCubit.isLogOut()); - }); - - test('onValidate calls isLogOut when user is not logged in', () { - when(() => mockAuthRepository.isLoggedIn()).thenReturn(false); - when(() => mockSessionCubit.isLogOut()).thenReturn(null); - - authService.onValidate(); - - verify(() => mockAuthRepository.isLoggedIn()).called(1); - verify(() => mockSessionCubit.isLogOut()).called(1); - verifyNever(() => mockSessionCubit.isLogin()); - }); - - test('onLogout calls logout and then isLogOut', () async { - when(() => mockAuthRepository.logout()).thenAnswer((_) async => {}); - when(() => mockSessionCubit.isLogOut()).thenReturn(null); - - await authService.onLogout(); - - verify(() => mockAuthRepository.logout()).called(1); - verify(() => mockSessionCubit.isLogOut()).called(1); - }); - }); -} From e5a9f5b4aed4cc7d8046f864c7e18b70f3d5e42b Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 12 Nov 2025 15:03:07 -0300 Subject: [PATCH 2/3] improve loading screen --- .github/pull_request_template.md | 2 - .github/workflows/rs-gpt-review.yml | 37 ------------------- .../ui/custom/loading_screen.dart | 28 ++++++-------- .../ui/pages/login/login_page.dart | 27 ++++++++------ app/melos.yaml | 3 -- melos.yaml | 6 ++- 6 files changed, 32 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/rs-gpt-review.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6a99dda..d2615a4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,5 +9,3 @@ #### Notes: [extra note or considerations] - -@rs-gpt-review Describe the changes in this PR. Recommend improvements (including code improvements), possible memory leaks, and best practices. diff --git a/.github/workflows/rs-gpt-review.yml b/.github/workflows/rs-gpt-review.yml deleted file mode 100644 index 501ffba..0000000 --- a/.github/workflows/rs-gpt-review.yml +++ /dev/null @@ -1,37 +0,0 @@ -# File: .github/workflows/rs-gpt-review.yml -name: 'rs-gpt-review' - -# Run the workflow on new issues, pull requests and comments -on: - issues: - types: [opened] - pull_request: - types: [opened] - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -# Allows the workflow to create comments on issues and pull requests -permissions: - issues: write - pull-requests: write - contents: write - -jobs: - # Runs for issues, pull requests and comments - rs-gpt-review: - name: rs-gpt-review comment - # Only run the job if the comment contains @rs-gpt-review - if: ${{ github.event_name == 'issues' && contains(github.event.issue.body, '@rs-gpt-review') || github.event_name == 'pull_request' && contains(github.event.pull_request.body, '@rs-gpt-review') || github.event_name == 'issue_comment' && contains(github.event.comment.body, '@rs-gpt-review') || github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@rs-gpt-review') }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - # The action will only run if the description or comments mentions @rs-gpt-review - - uses: rootstrap/rs-gpt-review@v2 - name: rs-gpt-review - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - openai_key: ${{ secrets.OPENAI_KEY }} - model: gpt-4-turbo - files_excluded: README.md, LICENSE, rs-gpt-review.yml, sonar-qube-scann.yml, .flutter-plugins, .flutter-plugins-dependencies diff --git a/app/lib/presentation/ui/custom/loading_screen.dart b/app/lib/presentation/ui/custom/loading_screen.dart index fd2ebd3..1883dad 100644 --- a/app/lib/presentation/ui/custom/loading_screen.dart +++ b/app/lib/presentation/ui/custom/loading_screen.dart @@ -1,32 +1,26 @@ import 'package:flutter/material.dart'; class LoadingScreen extends StatelessWidget { - final bool isLoading; final Color? color; const LoadingScreen({ super.key, - required this.isLoading, this.color, }); @override Widget build(BuildContext context) { - return Positioned.fill( - child: isLoading - ? Container( - color: Colors.transparent, - child: Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: color, - ), - ), - ), - ) - : const SizedBox.shrink(), + return Container( + color: Theme.of(context).colorScheme.primaryContainer.withAlpha(50), + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: color, + ), + ), + ), ); } } diff --git a/app/lib/presentation/ui/pages/login/login_page.dart b/app/lib/presentation/ui/pages/login/login_page.dart index d97c3b6..5c9b666 100644 --- a/app/lib/presentation/ui/pages/login/login_page.dart +++ b/app/lib/presentation/ui/pages/login/login_page.dart @@ -18,17 +18,14 @@ class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - Scaffold( - appBar: AppBar(), - backgroundColor: context.theme.colorScheme.surface, - body: Padding( + return Scaffold( + body: Stack( + children: [ + Padding( padding: EdgeInsets.all(spacing.m), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const _Loading(), const AppThemeSwitch(), SizedBox(height: spacing.m), SizedBox( @@ -50,8 +47,9 @@ class LoginPage extends StatelessWidget { ], ), ), - ), - ], + const _Loading(), + ], + ), ); } } @@ -63,8 +61,15 @@ class _Loading extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return LoadingScreen( - isLoading: state is RLoading, + if (state is! RLoading) { + return const SizedBox.shrink(); + } + + return Container( + color: Colors.black.withAlpha(50), + width: double.maxFinite, + height: double.maxFinite, + child: const LoadingScreen(), ); }, ); diff --git a/app/melos.yaml b/app/melos.yaml index 790b116..0923c00 100644 --- a/app/melos.yaml +++ b/app/melos.yaml @@ -5,9 +5,6 @@ packages: - modules/* scripts: - run:web: - exec: cd app && flutter run -t lib/main.dart --dart-define-from-file=env/.dev -d chrome - description: Run the app in development mode for web. lint:all: run: melos run analyze && melos run format description: Run all static analysis checks. diff --git a/melos.yaml b/melos.yaml index 62c14cf..10d1409 100644 --- a/melos.yaml +++ b/melos.yaml @@ -5,6 +5,10 @@ packages: - modules/* scripts: + run:web: + description: Run the app in development mode for web. + run: melos exec --scope="app" -- \ + flutter run -t lib/main.dart --dart-define-from-file=env/.dev -d chrome lint:all: run: melos run analyze && melos run format description: Run all static analysis checks. @@ -38,4 +42,4 @@ scripts: description: Run `dart doctor` in selected or all packages. Includes prompt for packages. packageFilters: dirExists: - - lib \ No newline at end of file + - lib From 7044d4c722858113742d60d10bc763d5b1f744ff Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 12 Nov 2025 15:33:04 -0300 Subject: [PATCH 3/3] fix copilot suggestions --- app/lib/presentation/ui/pages/home/home_view.dart | 2 +- .../presentation/ui/pages/splash/splash_page.dart | 2 +- modules/domain/lib/bloc/auth/auth_cubit.dart | 12 ++++-------- modules/domain/lib/init.dart | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/lib/presentation/ui/pages/home/home_view.dart b/app/lib/presentation/ui/pages/home/home_view.dart index 07d473e..1edcc29 100644 --- a/app/lib/presentation/ui/pages/home/home_view.dart +++ b/app/lib/presentation/ui/pages/home/home_view.dart @@ -5,7 +5,7 @@ import 'package:app/presentation/ui/custom/app_theme_switch.dart'; class HomeView extends StatelessWidget { /// Given this is a global cubit, we can access it directly from getIt - /// other wise use context.read() to read the Cubit under that context + /// otherwise use context.read() to read the Cubit under that context AuthCubit get _authCubit => getIt(); const HomeView({super.key}); diff --git a/app/lib/presentation/ui/pages/splash/splash_page.dart b/app/lib/presentation/ui/pages/splash/splash_page.dart index 6f9bb4c..9e8a0aa 100644 --- a/app/lib/presentation/ui/pages/splash/splash_page.dart +++ b/app/lib/presentation/ui/pages/splash/splash_page.dart @@ -11,7 +11,7 @@ class SplashPage extends StatefulWidget { class _SplashPageState extends State { /// Given this is a global cubit, we can access it directly from getIt - /// other wise use context.read() to read the Cubit under that context + /// otherwise use context.read() to read the Cubit under that context AuthCubit get _authCubit => getIt(); @override diff --git a/modules/domain/lib/bloc/auth/auth_cubit.dart b/modules/domain/lib/bloc/auth/auth_cubit.dart index 8c84600..27d53b1 100644 --- a/modules/domain/lib/bloc/auth/auth_cubit.dart +++ b/modules/domain/lib/bloc/auth/auth_cubit.dart @@ -8,18 +8,14 @@ class AuthCubit extends BaseCubit { final AuthService _authService; AuthCubit(this._authService) : super(RSuccess(data: AuthStateUnknown())); - Future login(String username, String password) async { + Future login(String username, String password) async { isLoading(); final authResult = await _authService.logInWithCredentials(username, password); - authResult.map( - success: (_) => isLogin(), - error: (failure) { - isError(failure); - return failure; - }, - ); + authResult + ..mapSuccess((_) => isLogin()) + ..mapError((failure) => isError(failure)); } Future onValidate() async { diff --git a/modules/domain/lib/init.dart b/modules/domain/lib/init.dart index 323ee9f..95f04fa 100644 --- a/modules/domain/lib/init.dart +++ b/modules/domain/lib/init.dart @@ -5,7 +5,7 @@ import 'package:get_it/get_it.dart'; class DomainInit { static Future initialize(GetIt getIt) async { - //Services (with dependencies to repositories) + //Services getIt.registerLazySingleton(() => AuthService(getIt())); //Global Cubits