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/home/home_view.dart b/app/lib/presentation/ui/pages/home/home_view.dart index d3ed9d9..1edcc29 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 + /// otherwise 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..5c9b666 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,18 +12,16 @@ 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( - 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, @@ -36,7 +33,7 @@ class LoginPage extends StatelessWidget { child: ElevatedButton( child: const Text('Login'), onPressed: () { - _authService.logInWithCredentials( + _authCubit.login( 'Rootstrap', '12345678', ); @@ -50,9 +47,9 @@ class LoginPage extends StatelessWidget { ], ), ), - ), - const _Loading(), - ], + const _Loading(), + ], + ), ); } } @@ -64,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/lib/presentation/ui/pages/splash/splash_page.dart b/app/lib/presentation/ui/pages/splash/splash_page.dart index a43fc5a..9e8a0aa 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 + /// otherwise 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/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 diff --git a/modules/domain/lib/bloc/auth/auth_cubit.dart b/modules/domain/lib/bloc/auth/auth_cubit.dart index 99f0f0d..27d53b1 100644 --- a/modules/domain/lib/bloc/auth/auth_cubit.dart +++ b/modules/domain/lib/bloc/auth/auth_cubit.dart @@ -1,9 +1,35 @@ 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 + ..mapSuccess((_) => isLogin()) + ..mapError((failure) => isError(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..95f04fa 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 - getIt.registerSingleton(AppCubit(getIt())); - getIt.registerSingleton(AuthCubit()); - //Services - getIt.registerLazySingleton(() => AuthService(getIt(), getIt())); + getIt.registerLazySingleton(() => AuthService(getIt())); + + //Global Cubits + getIt.registerSingleton(AppCubit(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); - }); - }); -}