From a934dcb9416f46b19c30251c6dc8bf1e43dce281 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 6 Feb 2022 23:36:01 -0500 Subject: [PATCH 01/11] Fix secure storage --- android/app/src/main/AndroidManifest.xml | 3 +- lib/core/managers/user_repository.dart | 54 +++++++++++++++++------- pubspec.yaml | 2 +- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a17f937c3..359de1b4d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,8 @@ android:name="io.flutter.app.FlutterApplication" android:label="ÉTS Mobile" android:icon="@mipmap/launcher_icon" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + android:allowBackup="false"> silentAuthenticate() async { - final String username = await _secureStorage.read(key: usernameSecureKey); - - if (username != null) { - final String password = await _secureStorage.read(key: passwordSecureKey); - - return authenticate( - username: username, password: password, isSilent: true); + String username; + String password; + try { + username = await _secureStorage.read(key: usernameSecureKey); + if (username != null) { + password = await _secureStorage.read(key: passwordSecureKey); + return await authenticate( + username: username, password: password, isSilent: true); + } + } on PlatformException catch (e, stacktrace) { + await _secureStorage.deleteAll(); + _analyticsService.logError( + tag, + "SilentAuthenticate - PlatformException(Handled) - ${e.toString()}", + e, + stacktrace); } - return false; } @@ -155,6 +164,7 @@ class UserRepository { await _secureStorage.delete(key: usernameSecureKey); await _secureStorage.delete(key: passwordSecureKey); } on PlatformException catch (e, stacktrace) { + await _secureStorage.deleteAll(); _analyticsService.logError(tag, "Authenticate - PlatformException - ${e.toString()}", e, stacktrace); return false; @@ -174,9 +184,15 @@ class UserRepository { throw const ApiException(prefix: tag, message: "Not authenticated"); } } - - final String password = await _secureStorage.read(key: passwordSecureKey); - + String password; + try { + password = await _secureStorage.read(key: passwordSecureKey); + } on PlatformException catch (e, stacktrace) { + await _secureStorage.deleteAll(); + _analyticsService.logError(tag, + "getPassword - PlatformException - ${e.toString()}", e, stacktrace); + throw const ApiException(prefix: tag, message: "Not authenticated"); + } return password; } @@ -295,11 +311,17 @@ class UserRepository { } 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; + try { + final String username = await _secureStorage.read(key: passwordSecureKey); + if (username != null) { + final String password = + await _secureStorage.read(key: passwordSecureKey); + return password.isNotEmpty; + } + } on PlatformException catch (e, stacktrace) { + await _secureStorage.deleteAll(); + _analyticsService.logError(tag, + "getPassword - PlatformException - ${e.toString()}", e, stacktrace); } return false; } diff --git a/pubspec.yaml b/pubspec.yaml index f9fe79745..0cf087333 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.7.2+1 +version: 4.7.3+1 environment: sdk: ">=2.10.0 <3.0.0" From 59036bb48ae8ebc269624bbccd8d422a4cf2b622 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 6 Feb 2022 23:47:38 -0500 Subject: [PATCH 02/11] refactor some variable --- lib/core/managers/user_repository.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/core/managers/user_repository.dart b/lib/core/managers/user_repository.dart index f0185a9fe..f4c26c22d 100644 --- a/lib/core/managers/user_repository.dart +++ b/lib/core/managers/user_repository.dart @@ -135,12 +135,10 @@ class UserRepository { /// Check if there are credentials saved and so authenticate the user, otherwise /// return false Future silentAuthenticate() async { - String username; - String password; try { - username = await _secureStorage.read(key: usernameSecureKey); + final username = await _secureStorage.read(key: usernameSecureKey); if (username != null) { - password = await _secureStorage.read(key: passwordSecureKey); + final password = await _secureStorage.read(key: passwordSecureKey); return await authenticate( username: username, password: password, isSilent: true); } @@ -184,16 +182,15 @@ class UserRepository { throw const ApiException(prefix: tag, message: "Not authenticated"); } } - String password; try { - password = await _secureStorage.read(key: passwordSecureKey); + final password = await _secureStorage.read(key: passwordSecureKey); + return password; } on PlatformException catch (e, stacktrace) { await _secureStorage.deleteAll(); _analyticsService.logError(tag, "getPassword - PlatformException - ${e.toString()}", e, stacktrace); throw const ApiException(prefix: tag, message: "Not authenticated"); } - return password; } /// Get the list of programs on which the student was active. From 99382e11d848ba92b2e2feeb079510e49e9368d4 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 20 Feb 2022 12:12:41 -0500 Subject: [PATCH 03/11] specify precise backup attribute (prevent disabling it for everything)) --- android/app/src/main/AndroidManifest.xml | 5 +- android/app/src/main/res/backup_rules.xml | 4 + lib/core/managers/user_repository.dart | 1 + pubspec.lock | 183 +++++++++------ pubspec.yaml | 2 +- test/managers/user_repository_test.dart | 215 ++++++++++++++---- .../services/flutter_secure_storage_mock.dart | 19 ++ 7 files changed, 309 insertions(+), 120 deletions(-) create mode 100644 android/app/src/main/res/backup_rules.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 359de1b4d..1f5ee6314 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,11 +18,12 @@ + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules"> + + + \ No newline at end of file diff --git a/lib/core/managers/user_repository.dart b/lib/core/managers/user_repository.dart index f4c26c22d..b734d50ee 100644 --- a/lib/core/managers/user_repository.dart +++ b/lib/core/managers/user_repository.dart @@ -307,6 +307,7 @@ class UserRepository { return _info; } + /// Check whether the user was previously authenticated. Future wasPreviouslyLoggedIn() async { try { final String username = await _secureStorage.read(key: passwordSecureKey); diff --git a/pubspec.lock b/pubspec.lock index 5bc0c4994..ee8ff0a2f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "29.0.0" + version: "34.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.6.0" + version: "3.2.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.6" + version: "3.2.1" args: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.1" built_collection: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.2" + version: "8.1.4" characters: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: connectivity_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.3.0" connectivity_plus_macos: dependency: transitive description: @@ -133,14 +133,14 @@ packages: name: connectivity_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" connectivity_plus_web: dependency: transitive description: name: connectivity_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.0+1" + version: "1.2.0" connectivity_plus_windows: dependency: transitive description: @@ -168,42 +168,42 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" dbus: dependency: transitive description: name: dbus url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.7.1" device_info_plus: dependency: "direct main" description: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.2.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0+1" device_info_plus_web: dependency: transitive description: @@ -217,7 +217,7 @@ packages: name: device_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" enum_to_string: dependency: "direct main" description: @@ -273,7 +273,7 @@ packages: name: firebase_analytics url: "https://pub.dartlang.org" source: hosted - version: "8.3.3" + version: "8.3.4" firebase_analytics_platform_interface: dependency: transitive description: @@ -294,35 +294,35 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.12.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.2.4" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.5.4" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.2.3" + version: "2.5.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.13" fixnum: dependency: transitive description: @@ -341,7 +341,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.3.0" flutter_config: dependency: "direct dev" description: @@ -388,7 +388,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.5" flutter_secure_storage: dependency: "direct main" description: @@ -440,7 +440,7 @@ packages: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "9.1.0" + version: "9.2.0" get_it: dependency: "direct main" description: @@ -454,7 +454,7 @@ packages: name: github url: "https://pub.dartlang.org" source: hosted - version: "8.2.0" + version: "8.5.0" glob: dependency: transitive description: @@ -468,14 +468,14 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.1.1" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.5" graphs: dependency: transitive description: @@ -503,7 +503,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.8" + version: "3.1.3" intl: dependency: transitive description: @@ -524,14 +524,14 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" lint: dependency: "direct dev" description: name: lint url: "https://pub.dartlang.org" source: hosted - version: "1.7.2" + version: "1.8.2" logger: dependency: "direct main" description: @@ -553,6 +553,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -566,7 +573,7 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "5.0.16" + version: "5.1.0" nested: dependency: transitive description: @@ -580,7 +587,7 @@ packages: name: nm url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.5.0" package_config: dependency: transitive description: @@ -594,7 +601,7 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" package_info_plus_linux: dependency: transitive description: @@ -657,35 +664,49 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.5" pedantic: dependency: transitive description: @@ -713,21 +734,21 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.2" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" provider: dependency: "direct main" description: @@ -748,35 +769,49 @@ packages: name: rive url: "https://pub.dartlang.org" source: hosted - version: "0.7.32" + version: "0.7.33" rxdart: dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.2" + version: "0.27.3" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -790,14 +825,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" simple_gesture_detector: dependency: transitive description: @@ -816,7 +851,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.2.1" source_span: dependency: transitive description: @@ -830,14 +865,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+4" + version: "2.0.2" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.0.1+1" + version: "2.2.0" stack_trace: dependency: transitive description: @@ -886,7 +921,7 @@ packages: name: table_calendar url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.3" term_glyph: dependency: transitive description: @@ -900,7 +935,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" typed_data: dependency: transitive description: @@ -921,42 +956,56 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.12" + version: "6.0.20" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.8" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" uuid: dependency: transitive description: @@ -991,42 +1040,42 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.15" + version: "2.8.3" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.8.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.7.1" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.9" + version: "2.4.1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: "direct main" description: @@ -1042,5 +1091,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.15.0 <3.0.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0cf087333..584c0b1f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.7.3+1 +version: 4.9.1+1 environment: sdk: ">=2.10.0 <3.0.0" diff --git a/test/managers/user_repository_test.dart b/test/managers/user_repository_test.dart index b4ce4db72..3e145a841 100644 --- a/test/managers/user_repository_test.dart +++ b/test/managers/user_repository_test.dart @@ -1,5 +1,6 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:convert'; +import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -32,7 +33,7 @@ import '../mock/services/signets_api_mock.dart'; void main() { AnalyticsService analyticsService; MonETSApi monETSApi; - FlutterSecureStorage secureStorage; + FlutterSecureStorageMock secureStorage; CacheManager cacheManager; SignetsApi signetsApi; NetworkingServiceMock networkingService; @@ -44,7 +45,8 @@ void main() { // Setup needed service analyticsService = setupAnalyticsServiceMock(); monETSApi = setupMonETSApiMock(); - secureStorage = setupFlutterSecureStorageMock(); + secureStorage = + setupFlutterSecureStorageMock() as FlutterSecureStorageMock; cacheManager = setupCacheManagerMock(); signetsApi = setupSignetsApiMock(); networkingService = setupNetworkingServiceMock() as NetworkingServiceMock; @@ -117,6 +119,36 @@ void main() { expect(manager.monETSUser, null, reason: "Verify the user stored should be null"); }); + + test('Verify that localstorage is safely deleted if an exception occurs', + () async { + final MonETSUser user = MonETSUser( + domain: "ENS", typeUsagerId: 1, username: "right credentials"); + + MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + FlutterSecureStorageMock.stubWriteException(secureStorage, + key: UserRepository.usernameSecureKey, + exceptionToThrow: PlatformException(code: "bad key")); + + // Result is false + expect( + await manager.authenticate(username: user.username, password: ""), + isFalse, + reason: "Check the authentication is successful"); + + // Verify the secureStorage is used + verify(secureStorage.write( + key: UserRepository.usernameSecureKey, value: user.username)); + + // Verify the user id is set in the analytics + verify(analyticsService.setUserProperties( + userId: user.username, domain: anyNamed("domain"))); + + expect(manager.monETSUser, user); + + // Verify the secureStorage is deleted + verify(secureStorage.deleteAll()); + }); }); group('authentication Signets - ', () { @@ -183,6 +215,7 @@ void main() { reason: "Verify the user stored should be null"); }); }); + group('silentAuthenticate - ', () { test('credentials are saved so the authentication should be done', () async { @@ -192,14 +225,10 @@ void main() { final MonETSUser user = MonETSUser(domain: "ENS", typeUsagerId: 1, username: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: password); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); @@ -222,14 +251,10 @@ void main() { const String username = "username"; const String password = "password"; - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: password); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); MonETSApiMock.stubAuthenticateException( monETSApi as MonETSApiMock, username); @@ -250,14 +275,10 @@ void main() { test('credentials are not saved so the authentication should not be done', () async { - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: null); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: null); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: null); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: null); expect(await manager.silentAuthenticate(), isFalse, reason: "Result should be false"); @@ -273,6 +294,27 @@ void main() { reason: "The authentication didn't happened so the user should be null"); }); + + test('Verify that localstorage is safely deleted if an exception occurs', + () async { + final MonETSUser user = MonETSUser( + domain: "ENS", typeUsagerId: 1, username: "right credentials"); + + MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + FlutterSecureStorageMock.stubReadException(secureStorage, + key: UserRepository.usernameSecureKey, + exceptionToThrow: PlatformException(code: "bad key")); + + // Result is false + expect(await manager.silentAuthenticate(), isFalse, + reason: "Result should be false"); + + verifyInOrder([ + secureStorage.read(key: UserRepository.usernameSecureKey), + secureStorage.deleteAll(), + analyticsService.logError(UserRepository.tag, any, any, any) + ]); + }); }); group('logOut - ', () { @@ -288,6 +330,22 @@ void main() { verifyNever( analyticsService.logError(UserRepository.tag, any, any, any)); }); + + test('Verify that localstorage is safely deleted if an exception occurs', + () async { + FlutterSecureStorageMock.stubDeleteException(secureStorage, + key: UserRepository.usernameSecureKey, + exceptionToThrow: PlatformException(code: "bad key")); + + expect(await manager.logOut(), isFalse); + + expect(manager.monETSUser, null, + reason: "The user shouldn't be available after a logout"); + + verify(secureStorage.delete(key: UserRepository.usernameSecureKey)); + verify(secureStorage.deleteAll()); + verify(analyticsService.logError(UserRepository.tag, any, any, any)); + }); }); group('getPassword - ', () { @@ -304,14 +362,10 @@ void main() { MonETSUser(domain: "ENS", typeUsagerId: 1, username: username); MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: password); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); expect(await manager.silentAuthenticate(), isTrue); @@ -337,14 +391,10 @@ void main() { MonETSUser(domain: "ENS", typeUsagerId: 1, username: username); MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: password); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); expect(await manager.getPassword(), password, reason: "Result should be 'password'"); @@ -367,14 +417,12 @@ void main() { MonETSApiMock.stubAuthenticateException( monETSApi as MonETSApiMock, username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.usernameSecureKey, - valueToReturn: username); - FlutterSecureStorageMock.stubRead( - secureStorage as FlutterSecureStorageMock, - key: UserRepository.passwordSecureKey, - valueToReturn: password); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); + + await manager.silentAuthenticate(); expect(manager.getPassword(), throwsA(isInstanceOf()), reason: @@ -386,6 +434,31 @@ void main() { verify(analyticsService.logError(UserRepository.tag, any, any, any)) .called(1); }); + + test('Verify that localstorage is safely deleted if an exception occurs', + () async { + const String username = "username"; + const String password = "password"; + + final MonETSUser user = + MonETSUser(domain: "ENS", typeUsagerId: 1, username: username); + + MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + FlutterSecureStorageMock.stubReadException(secureStorage, + key: UserRepository.passwordSecureKey, + exceptionToThrow: PlatformException(code: "bad key")); + + await manager.authenticate(username: username, password: password); + + expect(manager.getPassword(), throwsA(isInstanceOf()), + reason: "localStorage failed, should sent out a custom exception"); + + await untilCalled( + analyticsService.logError(UserRepository.tag, any, any, any)); + + verify(secureStorage.deleteAll()); + verify(analyticsService.logError(UserRepository.tag, any, any, any)); + }); }); group("getPrograms - ", () { @@ -721,5 +794,47 @@ void main() { expect(infoCache, info); }); }); + + group("wasPreviouslyLoggedIn - ", () { + test("check if username and password are present in the secure storage", + () async { + const String username = "username"; + const String password = "password"; + + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); + + expect(await manager.wasPreviouslyLoggedIn(), isTrue); + }); + + test("check when password is empty in secure storage", () async { + const String username = "username"; + const String password = ""; + + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.passwordSecureKey, valueToReturn: password); + + expect(await manager.wasPreviouslyLoggedIn(), isFalse); + }); + + test('Verify that localstorage is safely deleted if an exception occurs', + () async { + const String username = "username"; + + FlutterSecureStorageMock.stubRead(secureStorage, + key: UserRepository.usernameSecureKey, valueToReturn: username); + FlutterSecureStorageMock.stubReadException(secureStorage, + key: UserRepository.passwordSecureKey, + exceptionToThrow: PlatformException(code: "bad key")); + + expect(await manager.wasPreviouslyLoggedIn(), isFalse); + verify(secureStorage.deleteAll()); + verify(analyticsService.logError(UserRepository.tag, any, any, any)); + }); + }); }); } diff --git a/test/mock/services/flutter_secure_storage_mock.dart b/test/mock/services/flutter_secure_storage_mock.dart index 08929e248..d5da7d038 100644 --- a/test/mock/services/flutter_secure_storage_mock.dart +++ b/test/mock/services/flutter_secure_storage_mock.dart @@ -10,4 +10,23 @@ class FlutterSecureStorageMock extends Mock implements FlutterSecureStorage { {@required String key, @required String valueToReturn}) { when(mock.read(key: key)).thenAnswer((_) async => valueToReturn); } + + /// Stub the read function of [FlutterSecureStorage] with an [Exception] + static void stubReadException(FlutterSecureStorageMock mock, + {@required String key, @required Exception exceptionToThrow}) { + when(mock.read(key: key)).thenThrow(exceptionToThrow); + } + + /// Stub the write function of [FlutterSecureStorage] with an [Exception] + static void stubWriteException(FlutterSecureStorageMock mock, + {@required String key, @required Exception exceptionToThrow}) { + when(mock.write(key: key, value: anyNamed("value"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the delete function of [FlutterSecureStorage] with an [Exception] + static void stubDeleteException(FlutterSecureStorageMock mock, + {@required String key, @required Exception exceptionToThrow}) { + when(mock.delete(key: key)).thenThrow(exceptionToThrow); + } } From 03ade5464de33559768513a6accc7d1c98e673e4 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 20 Feb 2022 12:28:13 -0500 Subject: [PATCH 04/11] fix analyze warning --- lib/core/services/github_api.dart | 12 ++++++++++-- lib/core/viewmodels/dashboard_viewmodel.dart | 4 ++-- lib/core/viewmodels/profile_viewmodel.dart | 2 +- lib/ui/widgets/base_scaffold.dart | 2 +- test/managers/user_repository_test.dart | 9 ++++++--- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/core/services/github_api.dart b/lib/core/services/github_api.dart index 736919f65..44779240f 100644 --- a/lib/core/services/github_api.dart +++ b/lib/core/services/github_api.dart @@ -56,9 +56,13 @@ class GithubApi { CommitUser('clubapplets-server', 'clubapplets@gmail.com'), branch: 'main')) .catchError((error) { + // ignore: avoid_dynamic_calls _logger.e("uploadFileToGithub error: ${error.message}"); _analyticsService.logError( - tag, "uploadFileToGithub: ${error.message}", error as GitHubError); + tag, + // ignore: avoid_dynamic_calls + "uploadFileToGithub: ${error.message}", + error as GitHubError); }); } @@ -80,9 +84,13 @@ class GithubApi { "${await _internalInfoService.getDeviceInfoForErrorReporting()}", labels: ['bug', 'platform: ${Platform.operatingSystem}'])) .catchError((error) { + // ignore: avoid_dynamic_calls _logger.e("createGithubIssue error: ${error.message}"); _analyticsService.logError( - tag, "createGithubIssue: ${error.message}", error as GitHubError); + tag, + // ignore: avoid_dynamic_calls + "createGithubIssue: ${error.message}", + error as GitHubError); }); } diff --git a/lib/core/viewmodels/dashboard_viewmodel.dart b/lib/core/viewmodels/dashboard_viewmodel.dart index 8fe8873b4..2db8072ee 100644 --- a/lib/core/viewmodels/dashboard_viewmodel.dart +++ b/lib/core/viewmodels/dashboard_viewmodel.dart @@ -262,11 +262,11 @@ class DashboardViewModel extends FutureViewModel> { } Future> removeLaboratoryGroup( - List todayDateEvents) async { + List todayDateEvents) async { final List todayDateEventsCopy = List.from(todayDateEvents); for (final courseAcronym in todayDateEvents) { - final courseKey = courseAcronym.courseGroup.toString().split('-')[0]; + final courseKey = courseAcronym.courseGroup.split('-')[0]; final String activityCodeToUse = await _settingsManager.getDynamicString( PreferencesFlag.scheduleSettingsLaboratoryGroup, courseKey); diff --git a/lib/core/viewmodels/profile_viewmodel.dart b/lib/core/viewmodels/profile_viewmodel.dart index 77106061a..952d32e70 100644 --- a/lib/core/viewmodels/profile_viewmodel.dart +++ b/lib/core/viewmodels/profile_viewmodel.dart @@ -11,7 +11,7 @@ import 'package:notredame/core/managers/user_repository.dart'; import 'package:ets_api_clients/models.dart'; // OTHERS -import '../../locator.dart'; +import 'package:notredame/locator.dart'; class ProfileViewModel extends FutureViewModel> { /// Load the user diff --git a/lib/ui/widgets/base_scaffold.dart b/lib/ui/widgets/base_scaffold.dart index f837ab5aa..044683d9e 100644 --- a/lib/ui/widgets/base_scaffold.dart +++ b/lib/ui/widgets/base_scaffold.dart @@ -13,7 +13,7 @@ import 'package:notredame/ui/utils/app_theme.dart'; // WIDGETS import 'package:notredame/ui/widgets/bottom_bar.dart'; -import '../../locator.dart'; +import 'package:notredame/locator.dart'; /// Basic Scaffold to avoid boilerplate code in the application. /// Contains a loader controlled by [_isLoading] diff --git a/test/managers/user_repository_test.dart b/test/managers/user_repository_test.dart index fb4fb25dd..25a9f9ac0 100644 --- a/test/managers/user_repository_test.dart +++ b/test/managers/user_repository_test.dart @@ -126,7 +126,8 @@ void main() { final MonETSUser user = MonETSUser( domain: "ENS", typeUsagerId: 1, username: "right credentials"); - MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + MonETSAPIClientMock.stubAuthenticate( + monETSApi as MonETSAPIClientMock, user); FlutterSecureStorageMock.stubWriteException(secureStorage, key: UserRepository.usernameSecureKey, exceptionToThrow: PlatformException(code: "bad key")); @@ -306,7 +307,8 @@ void main() { final MonETSUser user = MonETSUser( domain: "ENS", typeUsagerId: 1, username: "right credentials"); - MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + MonETSAPIClientMock.stubAuthenticate( + monETSApi as MonETSAPIClientMock, user); FlutterSecureStorageMock.stubReadException(secureStorage, key: UserRepository.usernameSecureKey, exceptionToThrow: PlatformException(code: "bad key")); @@ -451,7 +453,8 @@ void main() { final MonETSUser user = MonETSUser(domain: "ENS", typeUsagerId: 1, username: username); - MonETSApiMock.stubAuthenticate(monETSApi as MonETSApiMock, user); + MonETSAPIClientMock.stubAuthenticate( + monETSApi as MonETSAPIClientMock, user); FlutterSecureStorageMock.stubReadException(secureStorage, key: UserRepository.passwordSecureKey, exceptionToThrow: PlatformException(code: "bad key")); From 70c2e1cf65a86d748a08bd5962d0803abeb3edfe Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 20 Feb 2022 12:43:04 -0500 Subject: [PATCH 05/11] update build with flutter to 2.10.x --- .github/workflows/main-workflow.yaml | 2 +- android/app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main-workflow.yaml b/.github/workflows/main-workflow.yaml index 372bcf0d2..5217fed51 100644 --- a/.github/workflows/main-workflow.yaml +++ b/.github/workflows/main-workflow.yaml @@ -75,7 +75,7 @@ jobs: java-version: '11' - uses: subosito/flutter-action@v1 with: - flutter-version: '2.5.x' + flutter-version: '2.10.x' channel: 'stable' - run: flutter doctor - name: Decrypt SignETS certificate and Google Services files diff --git a/android/app/build.gradle b/android/app/build.gradle index 3834b3931..5fbd5c11f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -70,7 +70,7 @@ android { signingConfig signingConfigs.release minifyEnabled true shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' firebaseCrashlytics { mappingFileUploadEnabled false } @@ -79,7 +79,7 @@ android { signingConfig signingConfigs.debug minifyEnabled true shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ext.enableCrashlytics = false ext.alwaysUpdateBuildId = false } From 713f67a048c2561ea5e21bbb873e5d49c98d7729 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 20 Feb 2022 12:58:57 -0500 Subject: [PATCH 06/11] [CI UPDATE GOLDENS] --- test/managers/user_repository_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/managers/user_repository_test.dart b/test/managers/user_repository_test.dart index 25a9f9ac0..3ea4183df 100644 --- a/test/managers/user_repository_test.dart +++ b/test/managers/user_repository_test.dart @@ -147,7 +147,6 @@ void main() { userId: user.username, domain: anyNamed("domain"))); expect(manager.monETSUser, user); - // Verify the secureStorage is deleted verify(secureStorage.deleteAll()); }); From 4fa8e228bf006a19018ac2eeef1cbd0b53ae0dfa Mon Sep 17 00:00:00 2001 From: MysticFragilist Date: Sun, 20 Feb 2022 18:03:39 +0000 Subject: [PATCH 07/11] Update golden files --- .../views/goldenFiles/quicksLinksView_1.png | Bin 7866 -> 8533 bytes test/ui/views/goldenFiles/scheduleView_1.png | Bin 2994 -> 2972 bytes test/ui/views/goldenFiles/scheduleView_2.png | Bin 2972 -> 2950 bytes test/ui/views/goldenFiles/scheduleView_3.png | Bin 3289 -> 3237 bytes test/ui/views/goldenFiles/scheduleView_4.png | Bin 3124 -> 3100 bytes test/ui/views/goldenFiles/scheduleView_5.png | Bin 3254 -> 3207 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/ui/views/goldenFiles/quicksLinksView_1.png b/test/ui/views/goldenFiles/quicksLinksView_1.png index 009bcb3a10ca6d47044274ab6673719f565c53ab..c5d15c14b54b649ec0dd24ef2351f171cc15d991 100644 GIT binary patch literal 8533 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 LX(}S0SpN23zV2El literal 7866 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 literal 2994 zcmc&$3pAT)8jey-HKVikVBDtdIJ)k0rY@Btt(tWmb*l(1!IVK=Dw+_I)R?$*+A)SO zMXH(!O$o)OIEdS#l~!7}Acz$aLEP$2*nivEc8_MJJ$v??{m=Q&fBx?~-}k-m^E~hK ze(A0*cHl#5hd>|@*umb`9R!k60Nw$4SzrXQT1EywQb>0@8&G+-h7kCXLE1QY$^%cL z{5NSJkita=+q0f=1)|ZjX$5@E&ZUs5>Y+!K7K4s9#|+hSn~&!OQolN6qiAIS|Fa_I zM!ScrfxUbLINa%~(C>gA!M>=*!k&m3x~ym=F9l!Jp?u#}Wk{o#R?6#mJ^Kb-BBx6? z(P=_YVMBsX7P3qiEMKyMH>To&fs$+6L(Ikl(VB%<8r;@yV&oC%BML{4E1$77xSr+> z`lJ$cJW$H^?+4P-4j%br--O6b5%l%xIHyvLt>y7OW51z!BG-R}8gMVH<1(&tym&pi zzL=RFHLn>*YQhidb45F)Dd`VB&D|uWfHITN%6QB~4rB`9Ji64AO4cg!YIN^H5lC^R z*{4b2LgZG(#5qw4tKEQK+e{4qDLH80xIRoVkcTDz6 z4*XRnG!Dye9$eKNIo5ME$ei;mYMOOIJ!%jR`V>f0x=B4S|4>#-x5Qk37lhXne{UM0R0*zP^A}Sg}*f=ksF(By?WsRGpcbS(mW4M85*h z#jX?0xrx=wJT3gr(w9(ZRC%62&&W;AUE(qOOQ~mmJxSP4ZbJcdOou%VI0-XFB9Z() zst1typ7ZiWKuW*%i%_W;r~(2O164Ts`73SNa~Dz;sTt=jFQCl!ELQ~LR%4uBF+)r% zf*NNz8H6pi`-`jQED}4P-&x4&g{luDTkxmAQg6$6NugWT(9n?RQqsH5gTY|Txw$L1 zl+Nw}U_Tbj8+gHB+dhkcQm~3yr_l2k>k>C(_?@FjmKFbb(K)cQjKmW8tSiq|j6;+O z6>reW%2uxNhE9fbXA=knS)HxyI$-Oc_L<4jKxneEiVAaLV&Z=1b)UELe&aPgGpPE; zaB-6QTClbClIQKs7f*`tY~ji|1%~SFfME$5m_$}UBDM^lU17hqhB2z>2SimUHoPI=K=QCozc3wTFd^ zb3@fw|4RqfVK_;ZWGK;D+wLKF?6PkEydstzQM~QB94UUrB!$ zkxHei={)GHZQKom^GCR-E~Tb=$RY%drC-2pKOg(oTs6zumkD7qne;M`R63nr#b8)3 zLe4|Qu@=$eXTVC}`HA))_yiR{)i~1jLQ|80fkEFFZdM(lRRPc-`xwx%UcJjv1k%Ww zi!Ka3bLSqeW)$U1;c*id8dG$rELzQ&Pbdc~?4iW)yNI$U)JC2@bslQM;}dt*)==?# z;&t_c`&zuktx_vxq(2pi=;}syAB17C z*g0d=Z0D(l-M5WZb0_6y^Rx00z^>j9)Uzv8_vM@8%XSYMatFD3_&QH9nk=@Sr!1V$ zUBF)AAUrz+rB&l<55}+wk&)#EP&}iO*D3lTEhY4LU?qd$=jG)!!OO?B^e;vDEikqt zoa%E&fa&I7m!hm}>$l)s& K+j1MkcmD(r)Y|(1 diff --git a/test/ui/views/goldenFiles/scheduleView_2.png b/test/ui/views/goldenFiles/scheduleView_2.png index 365e7f32bb2c93b933cf802e4c9e8b0cf719a5df..09a67e6f25bbf412babcc74f64521c9e6cec41ff 100644 GIT binary patch literal 2950 zcmdT`do)|=7LP~Ej6usR>fJJ9u3KtoRa-)-4$=(uN(z>SM}ktNS6sBH5hI>K1R*5EEBD;7s%HJn;EN0ESw8UKL0B3ag37vNX}}9V!qC)O5V*nxJ>ozh zp>Iu%46H*Q%?}&IJ)+9BFTpF){!xFvrz-Wj$+c2ai{gXvX2m0UCZgUF39%ubsyRRB z{KQY@pMM>FRj}wwH_?Xg4Wx^)!wY#|o;sO8DM;@rFX*wi7f*0;E6^Dnrk#qlmrfhe zS#CUAnbkf0U^QaOUo$*{G8{VM8A0#tcjBpwM&p{SkcnY}F7RVQClw_w8>!xlvjrW0 z0aA41F>2+Fi#u}c_#VT&g(&DZax=k!+JhC~a@X5nxXmRda%4K;$bG!RNE2amQU`U^ zX<8wd-LZOUU|nzImf$g|5cm=n&M0#inIY9WbA$SnzOcciSJ;d}ezAdMm3Uqo)#OM1 z6AdG#uhH(T_TB!>>SxlilV2SkkalXb2$Mx{P$KjWb^RW1pcJqB0`(4o3aotjS6#es zfFz{WG(FzsL<**L?EJJhssEYnj-d*o(qJLVb6;QQ0~jWRA-OFq7Ub^Vdx%HReE$NjxGj%ftl#;hKyFx)Ro(FBdPLPy&z~45ovJ!MLWYHx&ymm-UA$-vwN_=ae z$L&RZy*+JhaBwh2IdX|LnPv{FPq%e;#!C?HjRaMpt1cn}>Uw2@AJ?vuetTmOTaCdEt2>@rK$H;=`s4OopPnCq0jY|Zd z{Q5&i2kYdnIg}wUSL5EEboI^^k%#z_tcMRze`EIO1nUGBo;qDo2~vFSSF6)EZ3ogx z1#VJ1s$1Xo71X|onp;XCE;z!`j`kZ(nyCXnXvSX^e#%`W15uVhbx-Xo zb&Ure^#dF(aEaRzEu{55Z&W4V-?^^R*3ORR-K=Dsv;u~X|At3UXZuSr-KfQ80aD(T zSm{hb<)~fmeN?|*x>lZqq~x{2-r{gdM+YP;E6da67nuw!b|Y+Yj@Da9YH2a#6CZXt z11MszOgCq%404Q{vs*t^MhdKUXnuH3RJU9kG5@07nCV#PyiQdQ*U)@bIP{Fx@ZZ2E|Oh>O2)8JN9RWPGnJVoYrolsf?wN$F1&X%B}cYr01_ z0qVzS2z+Zek0e0N%}1CgfE33g8!or-p(5`e(i{PQAURm*C@zWe!e_(xKH5y!YxTz3&#HGKO1Z2_ZO=oDpg@H8l|2fJ{hsadGiG zuWGN%9|Ibka6Z1i!bG#u{=ZvUS!qG7lLJxQ{)!HKlRp-YMn{YWFL3QkH`%c$ugoHO z^~N29w>GYvsbBiX_0>1Fwl0w9a#g()^ICpysQ!{*{?52v0 zjEu0^xw;0_YDUBc`*pj&uP-K9f4iN&Cadn|{u+Hxb&1HFC&*SYOQ zWI*TI&FVCsm1GC-+1Pk{d%L469rWAAw%|KWv#Nf0VN}$Ee<29GJE8!CTnf8;WKgw< zgJ7xu$eC({ap;&?eVO@H;sCmw5YCk$AI;a)bH*IewqVhQ5C{Zyle3&Cr|;)%bPg(h zJfXU}x`IfQRTx>3DNFKSUSE7AO7g}vNGPD!7RQ*|8w_parjJB z0{qFqxWg7`Yiz}@coEqP*E2FQ#wXMr>#b>gkjXjKdnRHX=|9=!xxD}Y delta 1964 zcmZuxc{rQd8b@0(qp7NnjB3*s&lpupZ7pd~`zX?>QhRhzgIknZBch#la4pgMHPjI4 zmcg~8wnQYM_SRmpR*B`}P7)#`)Dp@4+Ri*Pf86st=lpTr_q^wQe#`qatQeMe3Fq*g z-KA@Z#Vga7GKv>ZbPGdk>n5o+<`d2~r}a$r@7kMmUd=jsAD--0`z>5_G^_N9SKi@w z$HwyKhwT;0j%qxZSvuvx4Jkm1PVEnVTlDD2nOtmBoE7coTK2DZ8P@F}CUf%!rF%(d?0Lj}I* zt-uk@gC3MewH|ZM&pez9%fc?t8tsqI!+A0vZM{kOFa~(b8t63?`DilOQv-4t83zvE zR+9@_EgWYog8MnVeL(Itc&WB!6ih?zQ;yd~0BiCl@ZmFLkpptSp@e@nEyGJjxkg4~ zB{D&T4v@=cxgvm<@^IV>Bv&^69+^yz;f|Ew9ZOtmh4fxFUi_^?>T>5+PS<=c zViioH%_LXk#|0j*qEHHgx^tn-0pUV-uUMP{Rq-zZad0rIM-$2)!EbIOkThrHy<9-GHiw?&faw{I1&*N(cssn<@Yo2 zRQ_wPbk6>zrKRvB>mD}B3OjAp@)q(|9y`?Q=Br89{;vbBbfo(;cFKyUjOt0=jpq(V zR5F+Ee3KC7ZXDkKR)zgh+>7TU6 z5HvHG!Y*~fAm%?09eeovN_u!nMWg)*r%d2`h!)O?!BJr|9Me~}gbQz{^V)A#hu^$; zv!~p>)B)Kgo$QaD^p<((Nt-&sT|b+4;thCYf94e{HM@^Wxic@UWl0L2wyeTZx3i3d z#3-e`xJg#WGkLUlgVIydPCK-+VQIOy{*<3x94!Ml2D`Wbj0!Y^{k|z_x!&lKtCCJz%m%^f=h`?v4$Grn`3YiBEgLM4a z22Mt~#p2cwsEEy=OSN;;-Uv+&zVI)*pt-YG*bJT_#xLaA8iv$)#CT;+xY3x$; zf7(b{@-KCjep!$HFoO}4Ii~slLH6H%mQhyXSGB4+YNwir#*3LPbiv^Li0wY#5n0Pl zJ+8F_a0z0f@fu2M-InYMl!6Wg-ha0eKS_(?#Nlh|WHjItidq+6!d?S&5Dm>2td}pZ zSP;m5Tc4>E7+QYdYlV z>O~4&)z{aTJkA4c-O*te*5G66V$v8uxlW~0 z-qHe23L37#BQ-%f^D*X*LVEOCWMySFRJx}V2!z_ECNKkaQD|q5i=MTHsz6ugJF^z) z%O&cGM9eC^w6DK^)bM*`*UH|q(f*8)W~&+YnUJUl#P?(II@f8ul4q&3_c52r3kLp8 zBSTsn^zEf0-CGO;bhDqCnc1=8AEVvf-9x03J)sb6ev$QICL;P-a(4SJWz(xKh5U}( zQGu&iKJsgrHcG#w@{SllSx$mwlD6j3b^V>s>gkQfe8GOfSZ%5pCU-nggoV@ZhrPSN zlztjQTDXx^&B>FT6@q4qngF4)TcZ+rup(Uhy|u?Em&={t@W=4C6Q!VuKhEN+NE%{m zpW59)MhWq6HgMVvrQFKrNXmC$^(jHP3h_qF1}SF~Goxtr_$)`-L~Sk09=smXYI+U3 zv)R|@Vvduf1a;%5V9HSTh3bq>E;roU$LF8;s2%9_Zdwv+eE_kzy-dR6t&&AQCaw3M zS4T*mro+!T;^8@Xj;p1Cz-l)csF8>1Pkwc>=Z9qr5O!}rzifO<^qdV@>Dz~Opunq0 v%{w;+bAn$%MHfAloDwVHdBACis;XGsPT%zx;tm`q1FrAv-R!Du0`L6~K&#?! diff --git a/test/ui/views/goldenFiles/scheduleView_3.png b/test/ui/views/goldenFiles/scheduleView_3.png index b11adc18a03d26a77554bea708298976f7eb9a81..08902021d4eae9e7c775e61677b14b73aeab9e54 100644 GIT binary patch literal 3237 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 OAjbo)b`|@Np8pg3dxXaT diff --git a/test/ui/views/goldenFiles/scheduleView_4.png b/test/ui/views/goldenFiles/scheduleView_4.png index 9bc8db28b00d60474a8277a43732c236283bebb1..fe71996153995e48aca333413f78fab01626e6ac 100644 GIT binary patch literal 3100 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 NIGu5KthEok`%m`gHLU;u diff --git a/test/ui/views/goldenFiles/scheduleView_5.png b/test/ui/views/goldenFiles/scheduleView_5.png index de02307c0eac6c1377c72208cb30d625d4f15493..48e0ba5565d49aad8c4ee13c9735374f597ad318 100644 GIT binary patch literal 3207 zcmeHKcTiK=9u6o?K`a9jqM)qhStu(CN=I2xRFD!t2_OPNX;G0HAPSGRg1}HB5D*qa zMB36zgwRW}MtVSmAVo?-B0@k&Lf%z(t@w22{rm3BnLFp6bI<*i@ArMb8+pOVKx~8b z1`r4&cILF6DG0>35g2{Kg20o@th_AX#^-5ja1vBN-Z2eK)_a~jV=fF_0m6LWk2N#AIAgze_#jsF*wlj_U9#Qbep6e%MSVq(g_%lU(jTNL-v91VZOi z)-oCf@p$BjSzcZ?Y`L>f&6{qOeE#vsf`ZcE4f!!VpsCg9gC;cEZ+j;?tTa$~r7;dJP}ef#c>{P=oCWVHELQ&%J@$pXn{{A+visCTL zO#`~=kA{AqFE0Lu7#{X|RH%ofukS_9-%}TQ6PxK1z*A2@U4BCT(#H}3h{P>NoUlA> zyeScFTwDE$$y-4$B^{zx}-?gFk9b@>nAUYEqo%*+jN^XP0Y7kY#khf+WzQJw|N8wDst zE@#wpJ5(vlMdR&ANMe*HP!5JIISqh@C8ws+kQg=tUq~OW!3Tp`%<=a4i8uq9X(VRQ z1D{VH!dC=?p;^~Zs7#tSf$P+Dqk8_4RFb~caR$KgU{?z$?{W-p9Rqs%>p z%_`VT0s+T;S57Hsyco4?M*60`n|7)JUEH}ekLu}cU}}UpZURh)QeH1jDy{#CCiCGN zNc$?M&uq4&vor#oz(j{fBm+7}fn zilQYqziG0o*3Os+QMaC87W!!O1|GV{1j3QaJ z2q9#iRi;ttTxR@YSXA5ErObGW;dSr4Z`*|-MD5Jvol~nP1V1&!;MH;HAaMeHi&~lA zdytgIu-uhW>i!IW$%e2ho0XL{u{emZn`4m_@OdyG)X~c;ZEbCjwb$hi)*yW-+kEyL qVW#}$;>&Izgk!G*;|JV7PF0GlU82^z2LeA6pfkT2=@p#39Q+?Y_g26F literal 3254 zcmeHKdo)|w9!{xNM?I#!Q|;AKrZ1zdTP;ewilSQ5dL*8SP*>|&;!%Rsbb8f7T_oO* zX;G!EcsBKl8Vp5)2@+D$dW=VqNr+S;$sBFh)KzPy^Zz|-?RC~Z`~3F#_P2lE_uD7# zvbDMNSO5G91OiE4urRd&fwo8iZBTL>@WhStqyV^WLD`ragUU$zXMoODl<@^SN#Kf< zym}V|+Ua(|^qk$zocWP}Yo1;)?W`$ljojw`E6$8+%l={Mm}j&~J7-m?^YJRm;<~*m z#-&lpHlYY?<$0Bo2Qt4}A(q<8h&S$A8u`W(Gv!+4e=?GW4i!~D+pAIa=v!Ro$vdHS zp1)eBFw{hT{zsa!(WJ(sFQ*pSd8tPs8gIcpI#YRQC+%pyaWTlHxoyC!D+UP>qaJ7# zyL(qc?rW)oDtnAfkKelM0Q$NDq~gBCbi+W$boo(GNWE|tx(|aEiPYgvOsqU9!0ulK zqh~yY?#+4J6=&HrbphEvDrvDZJD1%fC4x04M3Av7;GJ7kpnK-ja&0;zWYt(uRnNk6 zq9Pd-5JhCSE2fVjgwyu8{bF{W8H)jzPsbeD{T@vr6SYUQ*gIg~?Lp`DhK=rD{>MRg z&(DB;MC^aa0};*~cnu{k`xUpM$sO5l%iT3TA2 zlU?b2#i`sg2mA@(5NWvm!G?oBIfV z%hrrlHYyy%NtAmzID+rK~!Bgi3ALHogiS-;SnWMeqp2GUg1l9=C6EiEq+&oRN-Q+NOH)MaS#bliP;yX&i)r@}h}(AA^3L5IwtFTtw`L z9A7SPq4Og4)h@P(^P|%hG45{k_-52zv5!&=B)Kc;29B9JIyg{+_+w-4nHd>--y~3_ zKPBS^*|ug^F5j=Mt<})h)}Fgw-UYDyN!&}3-=hKp1I_8zA|fKp5$U=ivv##N-WpT< zVr7+1%B#cY>gJbe0qW!9<001b>QESru&UqP-F^Z zMNhglgSfTMmirpjQ(j#DG%hagJ{(F*%;NEflNKv0*LlnkOeuO%iEij9yeX_XfDtrB z3LR?S>Qn5_7W|vff<(iG{5GLk?X%+iMPHH zdYX^VKf%-s;yk241Y>t9m#PplNV8YVZ9P^v%)2*e?Z#*x0gq)0-L6ZXcE7xlzd8P6 z=?W*i5uO9zeKP2*g182~gJ@4AU)1QW z^QKL7^|3N(drJ4R#@l1{?+^j8rmd{3AR)JvH^JXpWUDn4pKcC=!Mc`~f+>Eswzkd& zF5U#+?95D66bj`^uy6a^xsr$T9}t~)j;YR8RaMm_j%-D?C1mr|KD8oq|9e9;8q%7L#YtImyY%zj)Vt zmvKd;n``ZeKrqoc(8u;>W@f-6&8%fE?B%gyVfhtB-pnMiy_PfblwANp*f%GXvZx+O zz!70}HS+Md9rF71lH=;?zzLy6*3jjiqP|me0xwLsy25VV4O!ABjfBx%?;QX`fw;cv zCd0M*%jU3RpsFxVd5Sar*d*I*pz6_R2*s5`7UGFKZ>!TKBu$D7nOw+0%CY#Q=A3Lx?NP!y_%=R-wG9g=e)2iBM5O$helwo%DJUCy^!9W;2a$8q(>WZVL6C8f>C?xOo67Q8Z-31=KGWl}@F?BoFmlH=r zzxBH4$JsH`NUGQ&yRVf03MSCI(Sd1ScZx1hLW^R8Mrd^bdcEGbL~E{^oYipXIM~S* zA*y1rSV7E2u78AJ*av??`}GYI54e(o0*&qE=3}0uI3F Date: Sun, 20 Feb 2022 13:18:18 -0500 Subject: [PATCH 08/11] update release CI --- .github/workflows/release-workflow.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-workflow.yaml b/.github/workflows/release-workflow.yaml index 77a5e7549..c75372a27 100644 --- a/.github/workflows/release-workflow.yaml +++ b/.github/workflows/release-workflow.yaml @@ -29,6 +29,9 @@ jobs: steps: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.10.x' + channel: 'stable' - name: Setup Fastlane uses: ruby/setup-ruby@v1 with: From 04f8c6410a2adae6ea4e6ebd5839fda335796073 Mon Sep 17 00:00:00 2001 From: Samuel Montambault Date: Sun, 20 Feb 2022 16:33:15 -0500 Subject: [PATCH 09/11] Update Android Manifest to target API 31 also update webview version --- android/app/build.gradle | 6 +++--- android/app/src/main/AndroidManifest.xml | 3 ++- android/app/src/main/res/{ => xml}/backup_rules.xml | 0 pubspec.lock | 2 +- pubspec.yaml | 2 +- scripts/decrypt.sh | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) rename android/app/src/main/res/{ => xml}/backup_rules.xml (100%) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5fbd5c11f..63c1f11c3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,7 +35,7 @@ apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -49,11 +49,11 @@ android { defaultConfig { applicationId "ca.etsmtl.applets.etsmobile" minSdkVersion 20 - targetSdkVersion 30 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true - manifestPlaceholders = [mapsApiKey: project.env.get("MAPS_API_KEY") ?:"$System.env.MAPS_API_KEY"] + manifestPlaceholders += [mapsApiKey: project.env.get("MAPS_API_KEY") ?:"$System.env.MAPS_API_KEY"] } signingConfigs { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1f5ee6314..01d64e79a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true">