diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..ee6ead88d Binary files /dev/null and b/.DS_Store differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b6b10bdf8..7f40e7dfc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,50 +1,71 @@ PODS: - - Firebase/Analytics (7.3.0): + - connectivity (0.0.1): + - Flutter + - Reachability + - Firebase/Analytics (7.11.0): - Firebase/Core - - Firebase/Core (7.3.0): + - Firebase/Core (7.11.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 7.3.0) - - Firebase/CoreOnly (7.3.0): - - FirebaseCore (= 7.3.0) - - Firebase/Crashlytics (7.3.0): + - FirebaseAnalytics (~> 7.11.0) + - Firebase/CoreOnly (7.11.0): + - FirebaseCore (= 7.11.0) + - Firebase/Crashlytics (7.11.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 7.3.0) - - firebase_analytics (8.0.0): - - Firebase/Analytics (= 7.3.0) + - FirebaseCrashlytics (~> 7.11.0) + - firebase_analytics (8.0.2): + - Firebase/Analytics (= 7.11.0) - firebase_core - Flutter - - firebase_core (1.0.3): - - Firebase/CoreOnly (= 7.3.0) + - firebase_core (1.1.0): + - Firebase/CoreOnly (= 7.11.0) - Flutter - - firebase_crashlytics (2.0.0): - - Firebase/Crashlytics (= 7.3.0) + - firebase_crashlytics (2.0.2): + - Firebase/Crashlytics (= 7.11.0) - firebase_core - Flutter - - FirebaseAnalytics (7.3.0): + - FirebaseAnalytics (7.11.0): + - FirebaseAnalytics/AdIdSupport (= 7.11.0) + - FirebaseCore (~> 7.0) + - FirebaseInstallations (~> 7.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/MethodSwizzler (~> 7.0) + - GoogleUtilities/Network (~> 7.0) + - "GoogleUtilities/NSData+zlib (~> 7.0)" + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/AdIdSupport (7.11.0): + - FirebaseAnalytics/Base (= 7.11.0) - FirebaseCore (~> 7.0) - FirebaseInstallations (~> 7.0) - - GoogleAppMeasurement (= 7.3.0) + - GoogleAppMeasurement/AdIdSupport (= 7.11.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.0) - GoogleUtilities/MethodSwizzler (~> 7.0) - GoogleUtilities/Network (~> 7.0) - "GoogleUtilities/NSData+zlib (~> 7.0)" - - nanopb (~> 2.30906.0) - - FirebaseCore (7.3.0): - - FirebaseCoreDiagnostics (~> 7.0) + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/Base (7.11.0): + - FirebaseCore (~> 7.0) + - FirebaseInstallations (~> 7.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/MethodSwizzler (~> 7.0) + - GoogleUtilities/Network (~> 7.0) + - "GoogleUtilities/NSData+zlib (~> 7.0)" + - nanopb (~> 2.30908.0) + - FirebaseCore (7.11.0): + - FirebaseCoreDiagnostics (~> 7.4) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/Logger (~> 7.0) - - FirebaseCoreDiagnostics (7.3.0): - - GoogleDataTransport (~> 8.0) + - FirebaseCoreDiagnostics (7.11.0): + - GoogleDataTransport (~> 8.4) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/Logger (~> 7.0) - - nanopb (~> 2.30906.0) - - FirebaseCrashlytics (7.3.0): + - nanopb (~> 2.30908.0) + - FirebaseCrashlytics (7.11.0): - FirebaseCore (~> 7.0) - FirebaseInstallations (~> 7.0) - - GoogleDataTransport (~> 8.0) - - nanopb (~> 2.30906.0) + - GoogleDataTransport (~> 8.4) + - nanopb (~> 2.30908.0) - PromisesObjC (~> 1.2) - - FirebaseInstallations (7.7.0): + - FirebaseInstallations (7.11.0): - FirebaseCore (~> 7.0) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/UserDefaults (~> 7.0) @@ -70,49 +91,52 @@ PODS: - google_maps_flutter (0.0.1): - Flutter - GoogleMaps (< 3.10) - - GoogleAppMeasurement (7.3.0): + - GoogleAppMeasurement/AdIdSupport (7.11.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.0) - GoogleUtilities/MethodSwizzler (~> 7.0) - GoogleUtilities/Network (~> 7.0) - "GoogleUtilities/NSData+zlib (~> 7.0)" - - nanopb (~> 2.30906.0) - - GoogleDataTransport (8.1.0): - - nanopb (~> 2.30906.0) + - nanopb (~> 2.30908.0) + - GoogleDataTransport (8.4.0): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (~> 1.2) - GoogleMaps (3.9.0): - GoogleMaps/Maps (= 3.9.0) - GoogleMaps/Base (3.9.0) - GoogleMaps/Maps (3.9.0): - GoogleMaps/Base - - GoogleUtilities/AppDelegateSwizzler (7.2.2): + - GoogleUtilities/AppDelegateSwizzler (7.4.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.2.2): + - GoogleUtilities/Environment (7.4.0): - PromisesObjC (~> 1.2) - - GoogleUtilities/Logger (7.2.2): + - GoogleUtilities/Logger (7.4.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.2.2): + - GoogleUtilities/MethodSwizzler (7.4.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.2.2): + - GoogleUtilities/Network (7.4.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.2.2)" - - GoogleUtilities/Reachability (7.2.2): + - "GoogleUtilities/NSData+zlib (7.4.0)" + - GoogleUtilities/Reachability (7.4.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.2.2): + - GoogleUtilities/UserDefaults (7.4.0): - GoogleUtilities/Logger - - nanopb (2.30906.0): - - nanopb/decode (= 2.30906.0) - - nanopb/encode (= 2.30906.0) - - nanopb/decode (2.30906.0) - - nanopb/encode (2.30906.0) + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) - OrderedSet (5.0.0) - package_info (0.0.1): - Flutter - path_provider (0.0.1): - Flutter - PromisesObjC (1.2.12) + - Reachability (3.2) - shared_preferences (0.0.1): - Flutter - sqflite (0.0.2): @@ -125,6 +149,7 @@ PODS: - Flutter DEPENDENCIES: + - connectivity (from `.symlinks/plugins/connectivity/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -157,9 +182,12 @@ SPEC REPOS: - nanopb - OrderedSet - PromisesObjC + - Reachability - Toast EXTERNAL SOURCES: + connectivity: + :path: ".symlinks/plugins/connectivity/ios" firebase_analytics: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: @@ -192,15 +220,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter/ios" SPEC CHECKSUMS: - Firebase: 26223c695fe322633274198cb19dca8cb7e54416 - firebase_analytics: b0d6f5a3c2208510a14b7e9f5b7f3a03b284a386 - firebase_core: b5d81dfd4fb2d6f700e67de34d9a633ae325c4e9 - firebase_crashlytics: 3b8018f6c88a59be95f9e2f38f04e5c0f05e0c6a - FirebaseAnalytics: 2580c2d62535ae7b644143d48941fcc239ea897a - FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972 - FirebaseCoreDiagnostics: d50e11039e5984d92c8a512be2395f13df747350 - FirebaseCrashlytics: d31325312c92e2cb2f0386d589b9aa44e303d99b - FirebaseInstallations: 42c86e7b02ff75b7f27f85833bf5dcb5f38a9774 + connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 + Firebase: c121feb35e4126c0b355e3313fa9b487d47319fd + firebase_analytics: 620e8cc1705feb6b9c40b6127bea9b39e03e3970 + firebase_core: 84dcd80ac6d29c3d1039071b7306ee99688eb9c7 + firebase_crashlytics: 1bf37e6bace6394a2bab8f316de395f49a836189 + FirebaseAnalytics: cd3bd84d722a24a8923918af8af8e5236f615d77 + FirebaseCore: 907447d8917a4d3eb0cce2829c5a0ad21d90b432 + FirebaseCoreDiagnostics: 68ad972f99206cef818230f3f3179d52ccfb7f8c + FirebaseCrashlytics: 272b675aa9d1e9bae1f9e1449fcc1f2cf6042806 + FirebaseInstallations: a58d4f72ec5861840b84df489f2668d970df558a Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_config: 2226c1df19c78fe34a05eb7f1363445f18e76fc1 flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 @@ -208,15 +237,16 @@ SPEC CHECKSUMS: fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a google_maps_flutter: c7f9c73576de1fbe152a227bfd6e6c4ae8088619 - GoogleAppMeasurement: 8d3c0aeede16ab7764144b5a4ca8e1d4323841b7 - GoogleDataTransport: 116c84c4bdeb76be2a7a46de51244368f9794eab + GoogleAppMeasurement: fd19169c3034975cb934e865e5667bfdce59df7f + GoogleDataTransport: cd9db2180fcecd8da1b561aea31e3e56cf834aa7 GoogleMaps: 4b5346bddfe6911bb89155d43c903020170523ac - GoogleUtilities: 31c5b01f978a70c6cff2afc6272b3f1921614b43 - nanopb: 1bf24dd71191072e120b83dd02d08f3da0d65e53 + GoogleUtilities: 284cddc7fffc14ae1907efb6f78ab95c1fccaedc + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/lib/core/managers/course_repository.dart b/lib/core/managers/course_repository.dart index 0dbec186d..8ef9c18ee 100644 --- a/lib/core/managers/course_repository.dart +++ b/lib/core/managers/course_repository.dart @@ -6,6 +6,7 @@ import 'package:logger/logger.dart'; // SERVICES import 'package:notredame/core/services/analytics_service.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/signets_api.dart'; import 'package:notredame/core/managers/cache_manager.dart'; import 'package:notredame/core/managers/user_repository.dart'; @@ -51,6 +52,9 @@ class CourseRepository { /// Cache manager to access and update the cache. final CacheManager _cacheManager = locator(); + /// Used to verify if the user has connectivity + final NetworkingService _networkingService = locator(); + /// Student list of courses List _courses; @@ -82,8 +86,14 @@ class CourseRepository { /// is updated with the latest version of the activities. Future> getCoursesActivities( {bool fromCacheOnly = false}) async { + // Force fromCacheOnly mode when user has no connectivity + if (!(await _networkingService.hasConnectivity())) { + // ignore: parameter_assignments + fromCacheOnly = true; + } + // Load the activities from the cache if the list doesn't exist - if (_coursesActivities == null) { + if (_coursesActivities == null ) { _coursesActivities = []; try { final List responseCache = @@ -96,15 +106,16 @@ class CourseRepository { .toList(); _logger.d( "$tag - getCoursesActivities: ${_coursesActivities.length} activities loaded from cache"); - if (fromCacheOnly) { - return _coursesActivities; - } } on CacheException catch (_) { _logger.e( "$tag - getCoursesActivities: exception raised will trying to load activities from cache."); } } + if (fromCacheOnly) { + return _coursesActivities; + } + final List fetchedCoursesActivities = []; try { @@ -205,8 +216,14 @@ class CourseRepository { /// Get the student's course list. After fetching the courses from [SignetsApi], /// the [CacheManager] is updated with the latest version of the courses. Future> getCourses({bool fromCacheOnly = false}) async { + // Force fromCacheOnly mode when user has no connectivity + if (!(await _networkingService.hasConnectivity())) { + // ignore: parameter_assignments + fromCacheOnly = true; + } + // Load the activities from the cache if the list doesn't exist - if (_courses == null || fromCacheOnly) { + if (_courses == null) { _courses = []; try { final List responseCache = @@ -219,15 +236,16 @@ class CourseRepository { .toList(); _logger.d( "$tag - getCourses: ${_courses.length} courses loaded from cache"); - if (fromCacheOnly) { - return _courses; - } } on CacheException catch (_) { _logger.e( "$tag - getCourses: exception raised will trying to load courses from cache."); } } + if (fromCacheOnly) { + return _courses; + } + final List fetchedCourses = []; try { diff --git a/lib/core/managers/user_repository.dart b/lib/core/managers/user_repository.dart index 8648724f3..f661008ca 100644 --- a/lib/core/managers/user_repository.dart +++ b/lib/core/managers/user_repository.dart @@ -9,6 +9,7 @@ import 'package:logger/logger.dart'; // SERVICES import 'package:notredame/core/services/analytics_service.dart'; import 'package:notredame/core/services/mon_ets_api.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/signets_api.dart'; import 'package:notredame/core/managers/cache_manager.dart'; @@ -42,6 +43,9 @@ class UserRepository { /// Will be used to report event and error. final AnalyticsService _analyticsService = locator(); + /// Used to verify if the user has connectivity + final NetworkingService _networkingService = locator(); + /// Secure storage manager to access and update the cache. final FlutterSecureStorage _secureStorage = locator(); @@ -154,6 +158,12 @@ class UserRepository { /// The list from the [CacheManager] is loaded than updated with the results /// from the [SignetsApi]. Future> getPrograms({bool fromCacheOnly = false}) async { + // Force fromCacheOnly mode when user has no connectivity + if (!(await _networkingService.hasConnectivity())) { + // ignore: parameter_assignments + fromCacheOnly = true; + } + // Load the programs from the cache if the list doesn't exist if (_programs == null) { try { @@ -169,15 +179,16 @@ class UserRepository { .toList(); _logger.d( "$tag - getPrograms: ${_programs.length} programs loaded from cache."); - if (fromCacheOnly) { - return _programs; - } } on CacheException catch (_) { _logger.e( "$tag - getPrograms: exception raised while trying to load the programs from cache."); } } + if (fromCacheOnly) { + return _programs; + } + try { // getPassword will try to authenticate the user if not authenticated. final String password = await getPassword(); @@ -211,6 +222,12 @@ class UserRepository { /// The information from the [CacheManager] is loaded than updated with the results /// from the [SignetsApi]. Future getInfo({bool fromCacheOnly = false}) async { + // Force fromCacheOnly mode when user has no connectivity + if (!(await _networkingService.hasConnectivity())) { + // ignore: parameter_assignments + fromCacheOnly = true; + } + // Load the student profile from the cache if the information doesn't exist if (_info == null) { try { @@ -220,15 +237,16 @@ class UserRepository { // Build info loaded from the cache. _info = ProfileStudent.fromJson(infoCached); _logger.d("$tag - getInfo: $_info info loaded from cache."); - if (fromCacheOnly) { - return _info; - } } on CacheException catch (_) { _logger.e( "$tag - getInfo: exception raised while trying to load the info from cache."); } } + if (fromCacheOnly) { + return _info; + } + try { // getPassword will try to authenticate the user if not authenticated. final String password = await getPassword(); @@ -254,4 +272,14 @@ class UserRepository { return _info; } + + Future wasPreviouslyLoggedIn() async { + final String username = await _secureStorage.read(key: usernameSecureKey); + + if (username != null) { + final String password = await _secureStorage.read(key: passwordSecureKey); + return password.isNotEmpty; + } + return false; + } } diff --git a/lib/core/services/github_api.dart b/lib/core/services/github_api.dart index 73f51b473..9a91d3cc0 100644 --- a/lib/core/services/github_api.dart +++ b/lib/core/services/github_api.dart @@ -9,12 +9,12 @@ import 'package:path_provider/path_provider.dart'; import 'package:flutter_config/flutter_config.dart'; // SERVICES +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/internal_info_service.dart'; -// OTHER +// OTHERS import 'package:notredame/locator.dart'; - class GithubApi { static const String tag = "GithubApi"; static const String tagError = "$tag - Error"; @@ -23,6 +23,9 @@ class GithubApi { static const String _repositorySlug = "ApplETS/Notre-Dame"; static const String _repositoryReportSlug = "ApplETS/Notre-Dame-Bug-report"; + /// Used to verify if the user has connectivity + final NetworkingService _networkingService = locator(); + GitHub _github; final InternalInfoService _internalInfoService = locator(); @@ -64,6 +67,12 @@ class GithubApi { "```$feedbackText```\n\n" "**Screenshot** \n" "![screenshot](https://github.com/$_repositoryReportSlug/blob/main/$fileName?raw=true)\n\n" + "**Device Infos** \n" + "- **Version:** ${packageInfo.version} \n" + "- **Connectivity:** ${await _networkingService.getConnectionType()} \n" + "- **Build number:** ${packageInfo.buildNumber} \n" + "- **Platform operating system:** ${Platform.operatingSystem} \n" + "- **Platform operating system version:** ${Platform.operatingSystemVersion} \n" "${await _internalInfoService.getDeviceInfoForErrorReporting()}", labels: ['bug', 'platform: ${Platform.operatingSystem}'])); } diff --git a/lib/core/services/networking_service.dart b/lib/core/services/networking_service.dart new file mode 100644 index 000000000..b269ffecc --- /dev/null +++ b/lib/core/services/networking_service.dart @@ -0,0 +1,17 @@ +import 'dart:async'; + +import 'package:connectivity/connectivity.dart'; + +class NetworkingService { + final Connectivity _connectivity = Connectivity(); + + Future hasConnectivity() async { + final connectionStatus = await _connectivity.checkConnectivity(); + return connectionStatus != ConnectivityResult.none; + } + + Future getConnectionType() async { + final connectionStatus = await _connectivity.checkConnectivity(); + return connectionStatus.toString(); + } +} diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index db958fb6e..f6c2e0d9b 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -3,6 +3,9 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +// SERVICES +import 'package:notredame/core/services/networking_service.dart'; + class Utils { /// Used to open a url static Future launchURL(String url, AppIntl intl) async { @@ -21,4 +24,11 @@ class Utils { return ((grade / maxGrade) * 100).roundToDouble(); } + + static Future showNoConnectionToast( + NetworkingService networkingService, AppIntl intl) async { + if (!await networkingService.hasConnectivity()) { + Fluttertoast.showToast(msg: intl.no_connectivity); + } + } } diff --git a/lib/core/viewmodels/grades_viewmodel.dart b/lib/core/viewmodels/grades_viewmodel.dart index c9f5cb22b..206fffa80 100644 --- a/lib/core/viewmodels/grades_viewmodel.dart +++ b/lib/core/viewmodels/grades_viewmodel.dart @@ -16,6 +16,12 @@ import 'package:notredame/core/models/course.dart'; // CONSTANTS import 'package:notredame/core/constants/router_paths.dart'; +// SERVICE +import 'package:notredame/core/services/networking_service.dart'; + +// UTILS +import 'package:notredame/core/utils/utils.dart'; + // OTHER import 'package:notredame/locator.dart'; @@ -29,6 +35,9 @@ class GradesViewModel extends FutureViewModel>> { /// Localization class of the application. final AppIntl _appIntl; + /// Verify if user has an active internet connection + final NetworkingService _networkingService = locator(); + /// Contains all the courses of the student sorted by session final Map> coursesBySession = {}; @@ -45,12 +54,13 @@ class GradesViewModel extends FutureViewModel>> { _buildCoursesBySession(coursesCached); // ignore: return_type_invalid_for_catch_error _courseRepository.getCourses().catchError(onError).then((value) { - if(value != null) { + if (value != null) { // Update the courses list _buildCoursesBySession(_courseRepository.courses); } }).whenComplete(() { setBusy(false); + Utils.showNoConnectionToast(_networkingService, _appIntl); }); return coursesBySession; @@ -94,9 +104,9 @@ class GradesViewModel extends FutureViewModel>> { if (a == b) return 0; // When the session is 's.o.' we put the course at the end of the list - if(a == "s.o.") { + if (a == "s.o.") { return 1; - } else if(b == "s.o.") { + } else if (b == "s.o.") { return -1; } diff --git a/lib/core/viewmodels/profile_viewmodel.dart b/lib/core/viewmodels/profile_viewmodel.dart index 200540b1d..9d0d1b21f 100644 --- a/lib/core/viewmodels/profile_viewmodel.dart +++ b/lib/core/viewmodels/profile_viewmodel.dart @@ -11,6 +11,12 @@ import 'package:notredame/core/managers/user_repository.dart'; import 'package:notredame/core/models/profile_student.dart'; import 'package:notredame/core/models/program.dart'; +// SERVICE +import 'package:notredame/core/services/networking_service.dart'; + +// UTILS +import 'package:notredame/core/utils/utils.dart'; + // OTHERS import '../../locator.dart'; @@ -21,6 +27,9 @@ class ProfileViewModel extends FutureViewModel> { /// Localization class of the application. final AppIntl _appIntl; + /// Verify if user has an active internet connection + final NetworkingService _networkingService = locator(); + /// List of the programs List _programList = List.empty(); @@ -37,7 +46,7 @@ class ProfileViewModel extends FutureViewModel> { String get universalAccessCode => _userRepository?.monETSUser?.universalCode ?? ''; - ProfileViewModel({@required AppIntl intl}): _appIntl = intl; + ProfileViewModel({@required AppIntl intl}) : _appIntl = intl; @override // ignore: type_annotate_public_apis @@ -70,7 +79,10 @@ class ProfileViewModel extends FutureViewModel> { .catchError(onError) // ignore: return_type_invalid_for_catch_error .then((value) => _userRepository.getPrograms().catchError(onError)) - .whenComplete(() => setBusyForObject(isLoadingEvents, false)); + .whenComplete(() { + setBusyForObject(isLoadingEvents, false); + Utils.showNoConnectionToast(_networkingService, _appIntl); + }); return value; }); @@ -79,11 +91,10 @@ class ProfileViewModel extends FutureViewModel> { setBusyForObject(isLoadingEvents, true); _userRepository .getInfo() - .then((value) => _userRepository.getPrograms() - .then((value) { - setBusyForObject(isLoadingEvents, false); - notifyListeners(); - })); + .then((value) => _userRepository.getPrograms().then((value) { + setBusyForObject(isLoadingEvents, false); + notifyListeners(); + })); } on Exception catch (error) { onError(error); } diff --git a/lib/core/viewmodels/schedule_viewmodel.dart b/lib/core/viewmodels/schedule_viewmodel.dart index e84113d49..8792edc8c 100644 --- a/lib/core/viewmodels/schedule_viewmodel.dart +++ b/lib/core/viewmodels/schedule_viewmodel.dart @@ -12,6 +12,12 @@ import 'package:notredame/core/managers/settings_manager.dart'; // MODELS import 'package:notredame/core/models/course_activity.dart'; +// SERVICE +import 'package:notredame/core/services/networking_service.dart'; + +// UTILS +import 'package:notredame/core/utils/utils.dart'; + // OTHER import 'package:notredame/locator.dart'; import 'package:notredame/core/constants/preferences_flags.dart'; @@ -38,6 +44,9 @@ class ScheduleViewModel extends FutureViewModel> { /// Get current locale Locale get locale => _settingsManager.locale; + /// Verify if user has an active internet connection + final NetworkingService _networkingService = locator(); + ScheduleViewModel({@required AppIntl intl, DateTime initialSelectedDate}) : _appIntl = intl, selectedDate = initialSelectedDate ?? DateTime.now(); @@ -65,6 +74,7 @@ class ScheduleViewModel extends FutureViewModel> { } }).whenComplete(() { setBusyForObject(isLoadingEvents, false); + Utils.showNoConnectionToast(_networkingService, _appIntl); }); return value; }); diff --git a/lib/core/viewmodels/startup_viewmodel.dart b/lib/core/viewmodels/startup_viewmodel.dart index ff32a01d8..c89c041d6 100644 --- a/lib/core/viewmodels/startup_viewmodel.dart +++ b/lib/core/viewmodels/startup_viewmodel.dart @@ -3,6 +3,7 @@ import 'package:stacked/stacked.dart'; // SERVICES / MANAGER import 'package:notredame/core/managers/user_repository.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/navigation_service.dart'; // MANAGER import 'package:notredame/core/managers/settings_manager.dart'; @@ -20,11 +21,15 @@ class StartUpViewModel extends BaseViewModel { /// Used to authenticate the user final UserRepository _userRepository = locator(); + /// Used to verify if the user has internet connectivity + final NetworkingService _networkingService = locator(); + /// Used to redirect on the dashboard. final NavigationService _navigationService = locator(); /// Try to silent authenticate the user then redirect to [LoginView] or [DashboardView] Future handleStartUp() async { + if (await handleConnectivityIssues()) return; final bool isLogin = await _userRepository.silentAuthenticate(); if (isLogin) { @@ -38,4 +43,18 @@ class StartUpViewModel extends BaseViewModel { } } } + + /// Verify if user has an active internet connection. If the user does have + /// an active internet connection, proceed with the normal app workflow. + /// Otherwise if the user was previously logged in, let him access the app + /// with the cached data + Future handleConnectivityIssues() async { + final hasConnectivityIssues = !await _networkingService.hasConnectivity(); + final wasLoggedIn = await _userRepository.wasPreviouslyLoggedIn(); + if (hasConnectivityIssues && wasLoggedIn) { + _navigationService.pushNamed(RouterPaths.dashboard); + return true; + } + return false; + } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6a0900ce5..5531b1bc5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -220,5 +220,7 @@ "discovery_navbar_ets_page_title": "ÉTS", "discovery_navbar_ets_page_details": "This page allows you to quickly consult all of the important ÉTS links, such as Moodle and MonÉTS.", "discovery_navbar_more_page_title": "More", - "discovery_navbar_more_page_details": "This page gives you access to additional options for the application." + "discovery_navbar_more_page_details": "This page gives you access to additional options for the application.", + + "no_connectivity": "No internet connection" } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index bebba2073..8e479b5bd 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -218,5 +218,7 @@ "discovery_navbar_ets_page_title": "ÉTS", "discovery_navbar_ets_page_details": "Cette page vous permet de consulter rapidement tous les liens importants de l’ÉTS, tels que Moodle et MonÉTS.", "discovery_navbar_more_page_title": "Plus", - "discovery_navbar_more_page_details": "Cette page vous permet d'avoir accès aux options supplémentaires de l'application." + "discovery_navbar_more_page_details": "Cette page vous permet d'avoir accès aux options supplémentaires de l'application.", + + "no_connectivity": "Aucune connexion internet" } diff --git a/lib/locator.dart b/lib/locator.dart index d328c83a4..55ee9b76e 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -8,6 +8,7 @@ import 'package:logger/logger.dart'; import 'package:notredame/core/services/navigation_service.dart'; import 'package:notredame/core/services/analytics_service.dart'; import 'package:notredame/core/services/mon_ets_api.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/preferences_service.dart'; import 'package:notredame/core/services/signets_api.dart'; import 'package:notredame/core/services/rive_animation_service.dart'; @@ -33,6 +34,7 @@ void setupLocator() { locator.registerLazySingleton(() => SignetsApi()); locator.registerLazySingleton(() => const FlutterSecureStorage()); locator.registerLazySingleton(() => PreferencesService()); + locator.registerLazySingleton(() => NetworkingService()); // Managers locator.registerLazySingleton(() => UserRepository()); diff --git a/pubspec.lock b/pubspec.lock index 105d2d190..eb5d12c29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,6 +106,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + connectivity: + dependency: "direct main" + description: + name: connectivity + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" convert: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c0f33493c..b1523bcf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: feature_discovery: ^0.14.0 path_provider: ^2.0.1 rive: ^0.7.1 + connectivity: ^3.0.3 dev_dependencies: flutter_test: diff --git a/test/helpers.dart b/test/helpers.dart index 4537a53f2..19f303398 100644 --- a/test/helpers.dart +++ b/test/helpers.dart @@ -21,6 +21,7 @@ import 'package:notredame/core/services/preferences_service.dart'; import 'package:notredame/core/managers/course_repository.dart'; import 'package:notredame/core/services/github_api.dart'; import 'package:notredame/core/managers/settings_manager.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/internal_info_service.dart'; // MOCKS @@ -34,6 +35,7 @@ import 'mock/services/github_api_mock.dart'; import 'mock/services/internal_info_service_mock.dart'; import 'mock/services/mon_ets_api_mock.dart'; import 'mock/services/navigation_service_mock.dart'; +import 'mock/services/networking_service_mock.dart'; import 'mock/services/preferences_service_mock.dart'; import 'mock/services/rive_animation_service_mock.dart'; import 'mock/services/signets_api_mock.dart'; @@ -101,7 +103,7 @@ void setupFlutterToastMock() { const MethodChannel channel = MethodChannel('PonnamKarthik/fluttertoast'); channel.setMockMethodCallHandler((MethodCall methodCall) async { - if(methodCall.method == 'showToast') { + if (methodCall.method == 'showToast') { return true; } }); @@ -228,3 +230,12 @@ CourseRepository setupCourseRepositoryMock() { return service; } + +NetworkingService setupNetworkingServiceMock() { + unregister(); + final service = NetworkingServiceMock(); + + locator.registerSingleton(service); + + return service; +} diff --git a/test/managers/course_repository_test.dart b/test/managers/course_repository_test.dart index a511b8019..749fb50ee 100644 --- a/test/managers/course_repository_test.dart +++ b/test/managers/course_repository_test.dart @@ -26,10 +26,12 @@ import '../helpers.dart'; // MOCKS import '../mock/managers/cache_manager_mock.dart'; import '../mock/managers/user_repository_mock.dart'; +import '../mock/services/networking_service_mock.dart'; import '../mock/services/signets_api_mock.dart'; void main() { AnalyticsService analyticsService; + NetworkingServiceMock networkingService; SignetsApi signetsApi; UserRepository userRepository; CacheManager cacheManager; @@ -43,6 +45,7 @@ void main() { signetsApi = setupSignetsApiMock(); userRepository = setupUserRepositoryMock(); cacheManager = setupCacheManagerMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; setupLogger(); manager = CourseRepository(); @@ -57,6 +60,8 @@ void main() { unregister(); clearInteractions(cacheManager); unregister(); + clearInteractions(networkingService); + unregister(); }); group("getCoursesActivities - ", () { @@ -100,6 +105,9 @@ void main() { CourseRepository.sessionsCacheKey, jsonEncode([])); SignetsApiMock.stubGetSessions( signetsApi as SignetsApiMock, username, [session]); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); test("Activities are loaded from cache.", () async { @@ -246,6 +254,8 @@ void main() { expect(manager.coursesActivities, isNull); expect(manager.getCoursesActivities(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.coursesActivities, isEmpty, reason: "The list of activities should be empty"); @@ -276,6 +286,8 @@ void main() { expect(manager.getCoursesActivities(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.coursesActivities, isEmpty, reason: "There isn't any activities saved in the cache so the list should be empty"); @@ -379,6 +391,8 @@ void main() { expect(manager.coursesActivities, isNull); expect(manager.getCoursesActivities(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.coursesActivities, isEmpty, reason: "The list of activities should be empty"); @@ -429,6 +443,21 @@ void main() { session: session.shortName) ]); }); + + test("Should force fromCacheOnly mode when user has no connectivity", + () async { + // Stub the cache to return 1 activity + CacheManagerMock.stubGet(cacheManager as CacheManagerMock, + CourseRepository.coursesActivitiesCacheKey, jsonEncode(activities)); + + //Stub the networkingService to return no connectivity + reset(networkingService); + NetworkingServiceMock.stubHasConnectivity(networkingService, + hasConnectivity: false); + + final activitiesCache = await manager.getCoursesActivities(); + expect(activitiesCache, activities); + }); }); group("getSessions - ", () { @@ -768,6 +797,9 @@ void main() { MonETSUser(domain: null, typeUsagerId: null, username: username)); UserRepositoryMock.stubGetPassword( userRepository as UserRepositoryMock, "password"); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); test("Courses are loaded from cache and cache is updated", () async { @@ -873,10 +905,6 @@ void main() { verifyInOrder([ cacheManager.get(CourseRepository.coursesCacheKey), - userRepository.getPassword(), - userRepository.monETSUser, - signetsApi.getCourses(username: username, password: password), - cacheManager.update(CourseRepository.coursesCacheKey, jsonEncode([])) ]); verifyNoMoreInteractions(signetsApi); @@ -893,6 +921,8 @@ void main() { expect(manager.courses, isNull); expect(manager.getCourses(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.courses, []); await untilCalled(analyticsService.logError(CourseRepository.tag, any)); @@ -1039,6 +1069,8 @@ void main() { expect(manager.sessions, isNull); expect(manager.getCourses(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.courses, [], reason: 'The courses list should be empty'); await untilCalled(analyticsService.logError(CourseRepository.tag, any)); @@ -1053,6 +1085,21 @@ void main() { username: anyNamed("username"), password: anyNamed("password"))); verifyNever(cacheManager.update(CourseRepository.coursesCacheKey, any)); }); + + test("Should force fromCacheOnly mode when user has no connectivity", + () async { + // Stub the cache to return 1 activity + CacheManagerMock.stubGet(cacheManager as CacheManagerMock, + CourseRepository.coursesCacheKey, jsonEncode([courseWithGrade])); + + //Stub the networkingService to return no connectivity + reset(networkingService); + NetworkingServiceMock.stubHasConnectivity(networkingService, + hasConnectivity: false); + + final coursesCache = await manager.getCourses(); + expect(coursesCache, [courseWithGrade]); + }); }); group("getCourseSummary - ", () { @@ -1103,6 +1150,9 @@ void main() { teacherMessage: '', ignore: false) ])); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); test("CourseSummary is fetched and cache is updated", () async { diff --git a/test/managers/user_repository_test.dart b/test/managers/user_repository_test.dart index a6c748a5f..9920715a3 100644 --- a/test/managers/user_repository_test.dart +++ b/test/managers/user_repository_test.dart @@ -9,6 +9,7 @@ import 'package:notredame/core/managers/user_repository.dart'; import 'package:notredame/core/managers/cache_manager.dart'; import 'package:notredame/core/services/mon_ets_api.dart'; import 'package:notredame/core/services/analytics_service.dart'; +import 'package:notredame/core/services/networking_service.dart'; import 'package:notredame/core/services/signets_api.dart'; // MODELS @@ -24,6 +25,7 @@ import '../helpers.dart'; import '../mock/managers/cache_manager_mock.dart'; import '../mock/services/flutter_secure_storage_mock.dart'; import '../mock/services/mon_ets_api_mock.dart'; +import '../mock/services/networking_service_mock.dart'; import '../mock/services/signets_api_mock.dart'; void main() { @@ -32,6 +34,7 @@ void main() { FlutterSecureStorage secureStorage; CacheManager cacheManager; SignetsApi signetsApi; + NetworkingServiceMock networkingService; UserRepository manager; @@ -43,6 +46,7 @@ void main() { secureStorage = setupFlutterSecureStorageMock(); cacheManager = setupCacheManagerMock(); signetsApi = setupSignetsApiMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; setupLogger(); manager = UserRepository(); @@ -56,6 +60,7 @@ void main() { unregister(); clearInteractions(signetsApi); unregister(); + unregister(); }); group('authentication - ', () { @@ -348,6 +353,9 @@ void main() { // Stub SignetsApi answer to test only the cache retrieving SignetsApiMock.stubGetPrograms( signetsApi as SignetsApiMock, username, []); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); test("Programs are loaded from cache", () async { @@ -442,6 +450,8 @@ void main() { expect(manager.programs, isNull); expect(manager.getPrograms(), throwsA(isInstanceOf())); + + await untilCalled(networkingService.hasConnectivity()); expect(manager.programs, [], reason: 'The programs list should be empty'); @@ -477,6 +487,17 @@ void main() { reason: 'The programs list should now be loaded even if the caching fails.'); }); + + test("Should force fromCacheOnly mode when user has no connectivity", + () async { + //Stub the networkingService to return no connectivity + reset(networkingService); + NetworkingServiceMock.stubHasConnectivity(networkingService, + hasConnectivity: false); + + final programsCache = await manager.getPrograms(); + expect(programsCache, programs); + }); }); group("getInfo - ", () { @@ -507,6 +528,9 @@ void main() { // Stub SignetsApi answer to test only the cache retrieving SignetsApiMock.stubGetInfo( signetsApi as SignetsApiMock, username, null); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); test("Info are loaded from cache", () async { @@ -635,6 +659,17 @@ void main() { expect(manager.info, info, reason: 'The info should now be loaded even if the caching fails.'); }); + + test("Should force fromCacheOnly mode when user has no connectivity", + () async { + //Stub the networkingService to return no connectivity + reset(networkingService); + NetworkingServiceMock.stubHasConnectivity(networkingService, + hasConnectivity: false); + + final infoCache = await manager.getInfo(); + expect(infoCache, info); + }); }); }); } diff --git a/test/mock/managers/user_repository_mock.dart b/test/mock/managers/user_repository_mock.dart index 678efdce1..c8805563f 100644 --- a/test/mock/managers/user_repository_mock.dart +++ b/test/mock/managers/user_repository_mock.dart @@ -97,4 +97,9 @@ class UserRepositoryMock extends Mock implements UserRepository { static void stubLogOut(UserRepositoryMock mock, {bool toReturn = true}) { when(mock.logOut()).thenAnswer((_) async => toReturn); } + + static void stubWasPreviouslyLoggedIn(UserRepositoryMock mock, + {bool toReturn = true}) { + when(mock.wasPreviouslyLoggedIn()).thenAnswer((_) async => toReturn); + } } diff --git a/test/mock/services/networking_service_mock.dart b/test/mock/services/networking_service_mock.dart new file mode 100644 index 000000000..3182176ad --- /dev/null +++ b/test/mock/services/networking_service_mock.dart @@ -0,0 +1,14 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:mockito/mockito.dart'; + +// SERVICE +import 'package:notredame/core/services/networking_service.dart'; + +/// Mock for the [NetworkingService] +class NetworkingServiceMock extends Mock implements NetworkingService { + /// Stub the user connection state + static void stubHasConnectivity(NetworkingServiceMock service, + {bool hasConnectivity = true}) { + when(service.hasConnectivity()).thenAnswer((_) async => hasConnectivity); + } +} diff --git a/test/ui/views/grades_view_test.dart b/test/ui/views/grades_view_test.dart index d8d4f2f38..59f805faa 100644 --- a/test/ui/views/grades_view_test.dart +++ b/test/ui/views/grades_view_test.dart @@ -16,9 +16,11 @@ import 'package:notredame/ui/widgets/grade_button.dart'; // OTHERS import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; +import '../../mock/services/networking_service_mock.dart'; void main() { CourseRepository courseRepository; + NetworkingServiceMock networkingService; AppIntl intl; final Course courseSummer = Course( @@ -63,10 +65,15 @@ void main() { setUp(() async { intl = await setupAppIntl(); setupNavigationServiceMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; courseRepository = setupCourseRepositoryMock(); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); tearDown(() { unregister(); + unregister(); }); group("golden -", () { testWidgets("No grades available", (WidgetTester tester) async { diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index c16df1954..bba7bd6a7 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -13,22 +13,30 @@ import '../../helpers.dart'; // MOCKS import '../../mock/managers/user_repository_mock.dart'; +import '../../mock/services/networking_service_mock.dart'; void main() { AppIntl intl; UserRepository userRepository; + NetworkingServiceMock networkingService; group('Profile view - ', () { setUp(() async { intl = await setupAppIntl(); setupNavigationServiceMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; userRepository = setupUserRepositoryMock(); UserRepositoryMock.stubGetInfo(userRepository as UserRepositoryMock); UserRepositoryMock.stubGetPrograms(userRepository as UserRepositoryMock); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); - tearDown(() {}); + tearDown(() { + unregister(); + }); testWidgets('contains student status', (WidgetTester tester) async { await tester.pumpWidget(localizedWidget(child: ProfileView())); diff --git a/test/ui/views/schedule_view_test.dart b/test/ui/views/schedule_view_test.dart index 9717d06f5..f7c58037d 100644 --- a/test/ui/views/schedule_view_test.dart +++ b/test/ui/views/schedule_view_test.dart @@ -22,10 +22,12 @@ import '../../helpers.dart'; // MOCKS import '../../mock/managers/course_repository_mock.dart'; import '../../mock/managers/settings_manager_mock.dart'; +import '../../mock/services/networking_service_mock.dart'; void main() { SettingsManager settingsManager; CourseRepository courseRepository; + NetworkingServiceMock networkingService; // Some activities CourseActivity activityYesterday; @@ -81,9 +83,13 @@ void main() { setupNavigationServiceMock(); settingsManager = setupSettingsManagerMock(); courseRepository = setupCourseRepositoryMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; SettingsManagerMock.stubLocale(settingsManager as SettingsManagerMock); + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); + settings = { PreferencesFlag.scheduleSettingsCalendarFormat: CalendarFormat.week, PreferencesFlag.scheduleSettingsStartWeekday: StartingDayOfWeek.monday, diff --git a/test/ui/views/student_view_test.dart b/test/ui/views/student_view_test.dart index ed944d307..481189a94 100644 --- a/test/ui/views/student_view_test.dart +++ b/test/ui/views/student_view_test.dart @@ -11,25 +11,34 @@ import 'package:notredame/ui/widgets/base_scaffold.dart'; import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; +import '../../mock/services/networking_service_mock.dart'; void main() { CourseRepository courseRepository; + NetworkingServiceMock networkingService; group('StudentView - ', () { setUp(() async { setupNavigationServiceMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; courseRepository = setupCourseRepositoryMock(); CourseRepositoryMock.stubCourses( courseRepository as CourseRepositoryMock); CourseRepositoryMock.stubGetCourses( - courseRepository as CourseRepositoryMock, fromCacheOnly: false); + courseRepository as CourseRepositoryMock, + fromCacheOnly: false); CourseRepositoryMock.stubGetCourses( - courseRepository as CourseRepositoryMock, fromCacheOnly: true); + courseRepository as CourseRepositoryMock, + fromCacheOnly: true); + + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); }); tearDown(() { unregister(); + unregister(); }); group('UI - ', () { diff --git a/test/viewmodels/grades_viewmodel_test.dart b/test/viewmodels/grades_viewmodel_test.dart index bc4466567..7c247bad9 100644 --- a/test/viewmodels/grades_viewmodel_test.dart +++ b/test/viewmodels/grades_viewmodel_test.dart @@ -18,12 +18,16 @@ import 'package:notredame/core/constants/router_paths.dart'; // OTHER import '../helpers.dart'; + +// MOCKS import '../mock/managers/course_repository_mock.dart'; +import '../mock/services/networking_service_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); NavigationService navigationService; CourseRepository courseRepository; + NetworkingServiceMock networkingService; AppIntl intl; GradesViewModel viewModel; @@ -81,21 +85,32 @@ void main() { 's.o.': [courseWithoutSession] }; - final courses = [courseSummer, courseSummer2, courseWinter, courseFall, courseWithoutSession]; + final courses = [ + courseSummer, + courseSummer2, + courseWinter, + courseFall, + courseWithoutSession + ]; group('GradesViewModel -', () { setUp(() async { courseRepository = setupCourseRepositoryMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; intl = await setupAppIntl(); navigationService = setupNavigationServiceMock(); setupFlutterToastMock(); + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); + viewModel = GradesViewModel(intl: intl); }); tearDown(() { unregister(); unregister(); + unregister(); tearDownFlutterToastMock(); }); diff --git a/test/viewmodels/profile_viewmodel_test.dart b/test/viewmodels/profile_viewmodel_test.dart index 45da6197d..df346c752 100644 --- a/test/viewmodels/profile_viewmodel_test.dart +++ b/test/viewmodels/profile_viewmodel_test.dart @@ -17,9 +17,11 @@ import '../helpers.dart'; // MOCKS import '../mock/managers/user_repository_mock.dart'; +import '../mock/services/networking_service_mock.dart'; UserRepository userRepository; SettingsManager settingsManager; +NetworkingServiceMock networkingService; ProfileViewModel viewModel; void main() { @@ -68,13 +70,18 @@ void main() { setUp(() async { // Setting up mocks userRepository = setupUserRepositoryMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; setupFlutterToastMock(); + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); + viewModel = ProfileViewModel(intl: await setupAppIntl()); }); tearDown(() { unregister(); + unregister(); tearDownFlutterToastMock(); }); diff --git a/test/viewmodels/schedule_viewmodel_test.dart b/test/viewmodels/schedule_viewmodel_test.dart index 2c878d1b6..5a35ebe3e 100644 --- a/test/viewmodels/schedule_viewmodel_test.dart +++ b/test/viewmodels/schedule_viewmodel_test.dart @@ -16,9 +16,11 @@ import '../helpers.dart'; // MOCKS import '../mock/managers/course_repository_mock.dart'; +import '../mock/services/networking_service_mock.dart'; CourseRepository courseRepository; SettingsManager settingsManager; +NetworkingServiceMock networkingService; ScheduleViewModel viewModel; void main() { @@ -57,14 +59,19 @@ void main() { // Setting up mocks courseRepository = setupCourseRepositoryMock(); settingsManager = setupSettingsManagerMock(); + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; setupFlutterToastMock(); + // Stub to simulate that the user has an active internet connection + NetworkingServiceMock.stubHasConnectivity(networkingService); + viewModel = ScheduleViewModel(intl: await setupAppIntl()); }); tearDown(() { unregister(); unregister(); + unregister(); tearDownFlutterToastMock(); }); diff --git a/test/viewmodels/startup_viewmodel_test.dart b/test/viewmodels/startup_viewmodel_test.dart index 460c4e935..7e5052c08 100644 --- a/test/viewmodels/startup_viewmodel_test.dart +++ b/test/viewmodels/startup_viewmodel_test.dart @@ -18,11 +18,13 @@ import 'package:notredame/core/viewmodels/startup_viewmodel.dart'; import '../helpers.dart'; import '../mock/managers/settings_manager_mock.dart'; import '../mock/managers/user_repository_mock.dart'; +import '../mock/services/networking_service_mock.dart'; void main() { NavigationService navigationService; UserRepositoryMock userRepositoryMock; SettingsManager settingsManager; + NetworkingServiceMock networkingService; StartUpViewModel viewModel; @@ -31,6 +33,7 @@ void main() { navigationService = setupNavigationServiceMock(); settingsManager = setupSettingsManagerMock(); userRepositoryMock = setupUserRepositoryMock() as UserRepositoryMock; + networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; setupLogger(); @@ -46,16 +49,24 @@ void main() { group('handleStartUp - ', () { test('sign in successful', () async { UserRepositoryMock.stubSilentAuthenticate(userRepositoryMock); + UserRepositoryMock.stubWasPreviouslyLoggedIn(userRepositoryMock); + NetworkingServiceMock.stubHasConnectivity(networkingService); await viewModel.handleStartUp(); verify(navigationService.pushNamed(RouterPaths.dashboard)); }); - test('sign in failed redirect to login if Discovery already been completed', () async { - UserRepositoryMock.stubSilentAuthenticate(userRepositoryMock, toReturn: false); + test( + 'sign in failed redirect to login if Discovery already been completed', + () async { + UserRepositoryMock.stubSilentAuthenticate(userRepositoryMock, + toReturn: false); + UserRepositoryMock.stubWasPreviouslyLoggedIn(userRepositoryMock); + NetworkingServiceMock.stubHasConnectivity(networkingService); - SettingsManagerMock.stubGetString(settingsManager as SettingsManagerMock, PreferencesFlag.welcome, + SettingsManagerMock.stubGetString( + settingsManager as SettingsManagerMock, PreferencesFlag.welcome, toReturn: 'true'); await viewModel.handleStartUp(); @@ -63,8 +74,13 @@ void main() { verify(navigationService.pushNamed(RouterPaths.login)); }); - test('sign in failed redirect to Choose Language page if Discovery has not been completed', () async { - UserRepositoryMock.stubSilentAuthenticate(userRepositoryMock, toReturn: false); + test( + 'sign in failed redirect to Choose Language page if Discovery has not been completed', + () async { + UserRepositoryMock.stubSilentAuthenticate(userRepositoryMock, + toReturn: false); + UserRepositoryMock.stubWasPreviouslyLoggedIn(userRepositoryMock); + NetworkingServiceMock.stubHasConnectivity(networkingService); await viewModel.handleStartUp();