From 6543776b701ef567f210132c9ec38b75a5dd76ee Mon Sep 17 00:00:00 2001 From: Hugo Migner Date: Fri, 8 Dec 2023 08:40:07 -0500 Subject: [PATCH 1/4] Add broadcast card in grades page --- lib/core/services/remote_config_service.dart | 6 ++ lib/core/viewmodels/grades_viewmodel.dart | 37 +++++++++ lib/ui/views/grades_view.dart | 80 ++++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/lib/core/services/remote_config_service.dart b/lib/core/services/remote_config_service.dart index 163926af7..dabb874e8 100644 --- a/lib/core/services/remote_config_service.dart +++ b/lib/core/services/remote_config_service.dart @@ -18,6 +18,7 @@ class RemoteConfigService { static const _privacyPolicyURL = "privacy_policy_url"; // dashboard message remote config keys + static const _gradesMsgToggle = "grades_message_toggle"; static const _dashboardMsgToggle = "dashboard_message_toggle"; static const _dashboardMsgFr = "dashboard_message_fr"; static const _dashboardMsgEn = "dashboard_message_en"; @@ -58,6 +59,11 @@ class RemoteConfigService { return _remoteConfig.getBool(_dashboardMsgToggle); } + bool get gradesMessageActive { + fetch(); + return _remoteConfig.getBool(_gradesMsgToggle); + } + bool get scheduleListViewDefault { fetch(); return _remoteConfig.getBool(_scheduleListViewDefault); diff --git a/lib/core/viewmodels/grades_viewmodel.dart b/lib/core/viewmodels/grades_viewmodel.dart index 1b2fb41d2..69e1f1fa7 100644 --- a/lib/core/viewmodels/grades_viewmodel.dart +++ b/lib/core/viewmodels/grades_viewmodel.dart @@ -6,6 +6,7 @@ import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; import 'package:stacked/stacked.dart'; // Project imports: @@ -19,6 +20,8 @@ import 'package:notredame/ui/utils/discovery_components.dart'; class GradesViewModel extends FutureViewModel>> { /// Used to get the courses of the student final CourseRepository _courseRepository = locator(); + final RemoteConfigService remoteConfigService = + locator(); /// Localization class of the application. final AppIntl _appIntl; @@ -30,11 +33,20 @@ class GradesViewModel extends FutureViewModel>> { /// session. final List sessionOrder = []; + /// Message to display in case of urgent/important broadcast need (Firebase + /// remote config), and the associated card title + String broadcastMessage = ""; + String broadcastTitle = ""; + String broadcastColor = ""; + String broadcastUrl = ""; + String broadcastType = ""; + GradesViewModel({@required AppIntl intl}) : _appIntl = intl; @override Future>> futureToRun() async => _courseRepository.getCourses(fromCacheOnly: true).then((coursesCached) { + futureToRunBroadcast(); setBusy(true); _buildCoursesBySession(coursesCached); // ignore: return_type_invalid_for_catch_error @@ -124,4 +136,29 @@ class GradesViewModel extends FutureViewModel>> { settingsManager.setBool(PreferencesFlag.discoveryStudentGrade, true); } } + + Future futureToRunBroadcast() async { + setBusyForObject(broadcastMessage, true); + setBusyForObject(broadcastTitle, true); + setBusyForObject(broadcastColor, true); + setBusyForObject(broadcastUrl, true); + setBusyForObject(broadcastType, true); + + if (_appIntl.localeName == "fr") { + broadcastMessage = remoteConfigService.dashboardMessageFr; + broadcastTitle = remoteConfigService.dashboardMessageTitleFr; + } else { + broadcastMessage = remoteConfigService.dashboardMessageEn; + broadcastTitle = remoteConfigService.dashboardMessageTitleEn; + } + broadcastColor = remoteConfigService.dashboardMsgColor; + broadcastUrl = remoteConfigService.dashboardMsgUrl; + broadcastType = remoteConfigService.dashboardMsgType; + + setBusyForObject(broadcastMessage, false); + setBusyForObject(broadcastTitle, false); + setBusyForObject(broadcastColor, false); + setBusyForObject(broadcastUrl, false); + setBusyForObject(broadcastType, false); + } } diff --git a/lib/ui/views/grades_view.dart b/lib/ui/views/grades_view.dart index e1d6730d6..ec3de36ff 100644 --- a/lib/ui/views/grades_view.dart +++ b/lib/ui/views/grades_view.dart @@ -1,4 +1,5 @@ // Flutter imports: +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -6,6 +7,9 @@ import 'package:flutter/scheduler.dart'; import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:notredame/core/constants/preferences_flags.dart'; +import 'package:notredame/core/viewmodels/dashboard_viewmodel.dart'; +import 'package:notredame/ui/widgets/dismissible_card.dart'; import 'package:stacked/stacked.dart'; // Project imports: @@ -97,6 +101,8 @@ class _GradesViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (model.remoteConfigService.gradesMessageActive && index == 0) + _buildMessageBroadcastCard(model), Text(sessionName, style: const TextStyle( fontSize: 25, @@ -114,6 +120,80 @@ class _GradesViewState extends State { ), ); + Widget _buildMessageBroadcastCard(GradesViewModel model) { + final broadcastMsgColor = Color(int.parse(model.broadcastColor)); + final broadcastMsgType = model.broadcastType; + final broadcastMsgUrl = model.broadcastUrl; + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: DismissibleCard( + key: UniqueKey(), + onDismissed: (DismissDirection direction) {}, + isBusy: model.busy(model.broadcastMessage), + cardColor: broadcastMsgColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(17, 10, 15, 20), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // title row + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text(model.broadcastTitle, + style: Theme.of(context).primaryTextTheme.headline6), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + child: + getBroadcastIcon(broadcastMsgType, broadcastMsgUrl), + ), + ), + ], + ), + // main text + AutoSizeText(model.broadcastMessage ?? "", + style: Theme.of(context).primaryTextTheme.bodyText2) + ]), + )), + ); + } + + Widget getBroadcastIcon(String type, String url) { + switch (type) { + case "warning": + return const Icon( + Icons.warning_rounded, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + case "alert": + return const Icon( + Icons.error, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + case "link": + return IconButton( + onPressed: () { + DashboardViewModel.launchBroadcastUrl(url); + }, + icon: const Icon( + Icons.open_in_new, + color: AppTheme.lightThemeBackground, + size: 30.0, + ), + ); + } + return const Icon( + Icons.campaign, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + } + /// Build the complete name of the session for the user local. String _sessionName(String shortName, AppIntl intl) { switch (shortName[0]) { From cd46d8ed40fb0fd1a2574781d277d1857141153b Mon Sep 17 00:00:00 2001 From: HugoMigner Date: Fri, 8 Dec 2023 13:42:44 +0000 Subject: [PATCH 2/4] [BOT] Applying version. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 51ecdbccd..5b3610368 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.33.1+1 +version: 4.34.0+1 environment: sdk: ">=2.10.0 <3.0.0" From 79680c1ef409bb23fcd6713f9d482d1edfaf8cf2 Mon Sep 17 00:00:00 2001 From: Hugo Migner Date: Fri, 8 Dec 2023 09:02:50 -0500 Subject: [PATCH 3/4] Fix tests and imports --- lib/ui/views/grades_view.dart | 1 - .../services/remote_config_service_mock.dart | 5 +++++ test/ui/views/grades_view_test.dart | 10 +++++++++ test/ui/views/student_view_test.dart | 21 +++++++++++++++++++ test/viewmodels/grades_viewmodel_test.dart | 7 +++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/ui/views/grades_view.dart b/lib/ui/views/grades_view.dart index ec3de36ff..8e950e7f9 100644 --- a/lib/ui/views/grades_view.dart +++ b/lib/ui/views/grades_view.dart @@ -7,7 +7,6 @@ import 'package:flutter/scheduler.dart'; import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:notredame/core/constants/preferences_flags.dart'; import 'package:notredame/core/viewmodels/dashboard_viewmodel.dart'; import 'package:notredame/ui/widgets/dismissible_card.dart'; import 'package:stacked/stacked.dart'; diff --git a/test/mock/services/remote_config_service_mock.dart b/test/mock/services/remote_config_service_mock.dart index f4eea8f4b..12e036884 100644 --- a/test/mock/services/remote_config_service_mock.dart +++ b/test/mock/services/remote_config_service_mock.dart @@ -12,6 +12,11 @@ class RemoteConfigServiceMock extends Mock implements RemoteConfigService { when(mock.scheduleListViewDefault).thenReturn(toReturn); } + static void stubGetGradesEnabled(RemoteConfigServiceMock mock, + {bool toReturn = true}) { + when(mock.gradesMessageActive).thenReturn(toReturn); + } + static void stubGetBroadcastEnabled(RemoteConfigServiceMock mock, {bool toReturn = true}) { when(mock.dashboardMessageActive).thenReturn(toReturn); diff --git a/test/ui/views/grades_view_test.dart b/test/ui/views/grades_view_test.dart index a1abdf0bd..1cd2b6cb0 100644 --- a/test/ui/views/grades_view_test.dart +++ b/test/ui/views/grades_view_test.dart @@ -14,13 +14,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:notredame/core/managers/course_repository.dart'; import 'package:notredame/core/managers/settings_manager.dart'; import 'package:notredame/core/services/networking_service.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; import 'package:notredame/ui/views/grades_view.dart'; import 'package:notredame/ui/widgets/grade_button.dart'; import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; +import '../../mock/services/remote_config_service_mock.dart'; void main() { CourseRepository courseRepository; + RemoteConfigService remoteConfigService; AppIntl intl; final Course courseSummer = Course( @@ -66,6 +69,7 @@ void main() { intl = await setupAppIntl(); setupNavigationServiceMock(); courseRepository = setupCourseRepositoryMock(); + remoteConfigService = setupRemoteConfigServiceMock(); setupSettingsManagerMock(); setupAnalyticsServiceMock(); }); @@ -136,6 +140,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, fromCacheOnly: true); + RemoteConfigServiceMock.stubGetGradesEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410); @@ -160,6 +167,9 @@ void main() { courseRepository as CourseRepositoryMock, toReturn: courses, fromCacheOnly: true); + RemoteConfigServiceMock.stubGetGradesEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410); diff --git a/test/ui/views/student_view_test.dart b/test/ui/views/student_view_test.dart index 1cc8c7421..06df631e6 100644 --- a/test/ui/views/student_view_test.dart +++ b/test/ui/views/student_view_test.dart @@ -12,20 +12,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:notredame/core/managers/course_repository.dart'; import 'package:notredame/core/managers/settings_manager.dart'; import 'package:notredame/core/services/networking_service.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; import 'package:notredame/ui/views/student_view.dart'; import 'package:notredame/ui/widgets/base_scaffold.dart'; import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; import '../../mock/services/analytics_service_mock.dart'; +import '../../mock/services/remote_config_service_mock.dart'; void main() { CourseRepository courseRepository; + RemoteConfigService remoteConfigService; group('StudentView - ', () { setUp(() async { setupNavigationServiceMock(); setupNetworkingServiceMock(); courseRepository = setupCourseRepositoryMock(); + remoteConfigService = setupRemoteConfigServiceMock(); setupSettingsManagerMock(); setupAnalyticsServiceMock(); @@ -37,6 +41,23 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, fromCacheOnly: true); + + RemoteConfigServiceMock.stubGetGradesEnabled( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastColor( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastEn( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastFr( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastTitleEn( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastTitleFr( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastType( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastUrl( + remoteConfigService as RemoteConfigServiceMock); }); tearDown(() { diff --git a/test/viewmodels/grades_viewmodel_test.dart b/test/viewmodels/grades_viewmodel_test.dart index d1e5df79f..bc6999e2f 100644 --- a/test/viewmodels/grades_viewmodel_test.dart +++ b/test/viewmodels/grades_viewmodel_test.dart @@ -8,13 +8,16 @@ import 'package:mockito/mockito.dart'; import 'package:notredame/core/managers/course_repository.dart'; import 'package:notredame/core/managers/settings_manager.dart'; import 'package:notredame/core/services/navigation_service.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; import 'package:notredame/core/viewmodels/grades_viewmodel.dart'; import '../helpers.dart'; import '../mock/managers/course_repository_mock.dart'; +import '../mock/services/remote_config_service_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); CourseRepository courseRepository; + RemoteConfigService remoteConfigService; AppIntl intl; GradesViewModel viewModel; @@ -84,10 +87,14 @@ void main() { group('GradesViewModel -', () { setUp(() async { courseRepository = setupCourseRepositoryMock(); + remoteConfigService = setupRemoteConfigServiceMock(); intl = await setupAppIntl(); setupSettingsManagerMock(); setupNavigationServiceMock(); + RemoteConfigServiceMock.stubGetGradesEnabled( + remoteConfigService as RemoteConfigServiceMock); + viewModel = GradesViewModel(intl: intl); }); From e3c2afe637ee8586e3103fb7ae33aa69f0f5a07c Mon Sep 17 00:00:00 2001 From: Hugo Migner Date: Fri, 8 Dec 2023 09:12:35 -0500 Subject: [PATCH 4/4] Fix test --- test/ui/views/grades_view_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ui/views/grades_view_test.dart b/test/ui/views/grades_view_test.dart index 1cd2b6cb0..96a01a6db 100644 --- a/test/ui/views/grades_view_test.dart +++ b/test/ui/views/grades_view_test.dart @@ -115,6 +115,9 @@ void main() { courseRepository as CourseRepositoryMock, toReturn: courses, fromCacheOnly: true); + RemoteConfigServiceMock.stubGetGradesEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410);