From 6b4275d0819a1ac48219946d4b3e41c252d58e03 Mon Sep 17 00:00:00 2001 From: Doan Thieu Date: Tue, 15 Aug 2023 14:26:17 +0700 Subject: [PATCH] [#8] Show no internet connection error dialog --- lib/l10n/app_en.arb | 4 ++- lib/screens/login/login_view_model.dart | 32 ++++++++++++++++--- lib/uimodels/app_error.dart | 14 ++++++-- lib/utils/internet_connection_manager.dart | 21 ++++++++++++ pubspec.lock | 8 +++++ pubspec.yaml | 1 + test/mocks/generate_mocks.dart | 2 ++ test/screens/login/login_view_model_test.dart | 32 +++++++++++++++++-- 8 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 lib/utils/internet_connection_manager.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e2c3bb3..fa28954 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -5,6 +5,8 @@ "loginButton": "Log in", "invalidEmailError": "Please enter the valid email format.", "invalidPasswordError": "Password must be at least 8 characters long.", - "unauthenticatedError": "Your email or password is incorrect.\nPlease try again.", + "unauthorizedError": "Your email or password is incorrect.\nPlease try again.", + "noInternetConnectionError": "You seem to be offline.\nPlease try again!", + "genericError": "Something went wrong.\nPlease try again!", "loginFailAlertTitle": "Unable to log in" } diff --git a/lib/screens/login/login_view_model.dart b/lib/screens/login/login_view_model.dart index 382f2d1..ed3234a 100644 --- a/lib/screens/login/login_view_model.dart +++ b/lib/screens/login/login_view_model.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:survey_flutter/uimodels/app_error.dart'; +import 'package:survey_flutter/utils/internet_connection_manager.dart'; final loginViewModelProvider = AsyncNotifierProvider.autoDispose(LoginViewModel.new); class LoginViewModel extends AutoDisposeAsyncNotifier { + late InternetConnectionManager internetConnectionManager; + bool isValidEmail(String? email) { // Just use a simple rule, no fancy Regex! return !(email == null || !email.contains('@')); @@ -19,11 +22,30 @@ class LoginViewModel extends AutoDisposeAsyncNotifier { login({required String email, required String password}) async { state = const AsyncLoading(); // TODO: Integrate with API - // For now, assume its returns error - state = const AsyncError( - AppError.unauthenticated, - StackTrace.empty, - ); + + // Handling error part: + + // If it returns unauthorized error (401) + //state = const AsyncError( + // AppError.unauthorized, + // StackTrace.empty, + //); + + // If it returns timeout error, then check Internet connection + internetConnectionManager = ref.read(internetConnectionManagerProvider); + final isConnected = await internetConnectionManager.hasConnection(); + + if (!isConnected) { + state = const AsyncError( + AppError.noInternetConnection, + StackTrace.empty, + ); + } else { + state = const AsyncError( + AppError.generic, + StackTrace.empty, + ); + } } @override diff --git a/lib/uimodels/app_error.dart b/lib/uimodels/app_error.dart index 4fdede1..f72d4a7 100644 --- a/lib/uimodels/app_error.dart +++ b/lib/uimodels/app_error.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; import 'package:survey_flutter/utils/build_context_ext.dart'; -enum AppError { unauthenticated } +enum AppError { + unauthorized, + noInternetConnection, + generic, +} extension AppErrorExtension on AppError { String description(BuildContext context) { switch (this) { - case AppError.unauthenticated: - return context.localizations.unauthenticatedError; + case AppError.unauthorized: + return context.localizations.unauthorizedError; + case AppError.noInternetConnection: + return context.localizations.noInternetConnectionError; + case AppError.generic: + return context.localizations.genericError; } } } diff --git a/lib/utils/internet_connection_manager.dart b/lib/utils/internet_connection_manager.dart new file mode 100644 index 0000000..5178a3a --- /dev/null +++ b/lib/utils/internet_connection_manager.dart @@ -0,0 +1,21 @@ +import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +abstract class InternetConnectionManager { + Future hasConnection(); +} + +final internetConnectionManagerProvider = + Provider((_) { + return InternetConnectionManagerImpl(); +}); + +class InternetConnectionManagerImpl extends InternetConnectionManager { + final InternetConnectionChecker _internetConnectionChecker = + InternetConnectionChecker(); + + @override + Future hasConnection() async { + return await _internetConnectionChecker.hasConnection; + } +} diff --git a/pubspec.lock b/pubspec.lock index 011b52d..d2845f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -404,6 +404,14 @@ packages: description: flutter source: sdk version: "0.0.0" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c81940c..8f1b05a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: japx: ^2.0.5 equatable: ^2.0.0 injectable: ^1.5.0 + internet_connection_checker: ^1.0.0+1 dev_dependencies: build_runner: ^2.4.4 diff --git a/test/mocks/generate_mocks.dart b/test/mocks/generate_mocks.dart index 553d942..ceaec26 100644 --- a/test/mocks/generate_mocks.dart +++ b/test/mocks/generate_mocks.dart @@ -3,12 +3,14 @@ import 'package:mockito/annotations.dart'; import 'package:survey_flutter/api/authentication_api_service.dart'; import 'package:survey_flutter/repositories/authentication_repository.dart'; import 'package:survey_flutter/utils/async_listener.dart'; +import 'package:survey_flutter/utils/internet_connection_manager.dart'; @GenerateMocks([ AsyncListener, AuthenticationApiService, AuthenticationRepository, DioError, + InternetConnectionManager, ]) main() { // empty class to generate mock repository classes diff --git a/test/screens/login/login_view_model_test.dart b/test/screens/login/login_view_model_test.dart index 5788e46..4156ccd 100644 --- a/test/screens/login/login_view_model_test.dart +++ b/test/screens/login/login_view_model_test.dart @@ -2,16 +2,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:survey_flutter/screens/login/login_view_model.dart'; +import 'package:survey_flutter/utils/internet_connection_manager.dart'; import '../../mocks/generate_mocks.mocks.dart'; void main() { group('LoginViewModel', () { late ProviderContainer container; + late MockInternetConnectionManager mockInternetConnectionManager; late MockAsyncListener listener; setUp(() { - container = ProviderContainer(); + mockInternetConnectionManager = MockInternetConnectionManager(); + container = ProviderContainer(overrides: [ + internetConnectionManagerProvider + .overrideWithValue(mockInternetConnectionManager), + ]); + listener = MockAsyncListener(); container.listen( loginViewModelProvider, @@ -76,9 +83,30 @@ void main() { }); }); + // TODO: Update when integrating with API group('login', () { - test('When logging in unsuccessfully, it emits error correspondingly', + // test('When logging in unsuccessfully, it emits error correspondingly', + // () async { + // const data = AsyncData(null); + // // verify initial value from build method + // verify(listener(null, data)); + + // final loginViewModel = container.read(loginViewModelProvider.notifier); + // await loginViewModel.login( + // email: 'user@test.com', password: '12345678'); + + // verifyInOrder([ + // listener(data, isA()), + // listener(isA>(), isA>()), + // ]); + // verifyNoMoreInteractions(listener); + // }); + + test( + 'When logging in without Internet connection, it emits error correspondingly', () async { + when(mockInternetConnectionManager.hasConnection()) + .thenAnswer((_) async => false); const data = AsyncData(null); // verify initial value from build method verify(listener(null, data));