From 25a603474c8102660468ed20ce069581ab519644 Mon Sep 17 00:00:00 2001
From: Akos Hermann <72087159+hermannakos@users.noreply.github.com>
Date: Wed, 2 Dec 2020 16:59:31 +0100
Subject: [PATCH] [MBL-14971][Parent] Manage remote config flags (#1128)
* [MBL-14971][Parent] Added the Remote config screen
refs: MBL-14971
affects: Parent
release note: Added the Remote config params screen to modify remote configs.
test plan: In debug mode, under settings a new menu is available 'Remote Config Params'. In the new screen you can update the local remote config values.
---
.../remote_config_interactor.dart | 26 +++++++
.../remote_config/remote_config_screen.dart | 72 +++++++++++++++++++
.../screens/settings/settings_interactor.dart | 5 ++
.../lib/screens/settings/settings_screen.dart | 17 ++++-
.../lib/utils/remote_config_utils.dart | 18 +++--
.../lib/utils/service_locator.dart | 2 +
.../remote_config_interactor_test.dart | 31 ++++++++
.../remote_config_screen_test.dart | 50 +++++++++++++
.../settings/settings_interactor_test.dart | 14 ++++
.../settings/settings_screen_test.dart | 16 +++++
.../test/utils/remote_config_utils_test.dart | 11 +++
11 files changed, 255 insertions(+), 7 deletions(-)
create mode 100644 apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart
create mode 100644 apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart
create mode 100644 apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart
create mode 100644 apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart
diff --git a/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart b/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart
new file mode 100644
index 0000000000..39c6fa40df
--- /dev/null
+++ b/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart
@@ -0,0 +1,26 @@
+// Copyright (C) 2020 - present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter_parent/utils/remote_config_utils.dart';
+
+class RemoteConfigInteractor {
+
+ Map getRemoteConfigParams() {
+ return Map.fromIterable(RemoteConfigParams.values, key: (rc) => rc, value: (rc) => RemoteConfigUtils.getStringValue(rc));
+ }
+
+ void updateRemoteConfig(RemoteConfigParams rcKey, String value) {
+ RemoteConfigUtils.updateRemoteConfig(rcKey, value);
+ }
+}
\ No newline at end of file
diff --git a/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart
new file mode 100644
index 0000000000..a0e0dc4858
--- /dev/null
+++ b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart
@@ -0,0 +1,72 @@
+// Copyright (C) 2020 - present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter/material.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart';
+import 'package:flutter_parent/utils/remote_config_utils.dart';
+import 'package:flutter_parent/utils/service_locator.dart';
+
+class RemoteConfigScreen extends StatefulWidget {
+ @override
+ _RemoteConfigScreenState createState() => _RemoteConfigScreenState();
+}
+
+class _RemoteConfigScreenState extends State {
+ RemoteConfigInteractor _interactor = locator();
+
+ Map _remoteConfig;
+
+ @override
+ void initState() {
+ _remoteConfig = _interactor.getRemoteConfigParams();
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text('Remote Config Params')),
+ body: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: ListView(
+ key: Key('remote_config_params_list'),
+ children: _createListItems(_remoteConfig),
+ ),
+ ),
+ );
+ }
+
+ List _createListItems(Map remoteConfigParams) {
+ return remoteConfigParams.entries.map((e) => _createListItem(e)).toList();
+ }
+
+ Widget _createListItem(MapEntry entry) {
+ return Row(children: [
+ Align(
+ child: Text(RemoteConfigUtils.getRemoteConfigName(entry.key)),
+ alignment: Alignment.centerLeft,
+ ),
+ Flexible(
+ child: Padding(
+ padding: EdgeInsets.only(left: 8.0),
+ child: TextField(
+ controller: TextEditingController()..text = entry.value,
+ onChanged: (value) => {
+ _interactor.updateRemoteConfig(entry.key, value)
+ },
+ ),
+ ))
+ ]);
+ }
+}
diff --git a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart
index 04f5ec93ca..5eb14752d1 100644
--- a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart
+++ b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart
@@ -14,6 +14,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_parent/network/utils/analytics.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart';
import 'package:flutter_parent/utils/debug_flags.dart';
import 'package:flutter_parent/utils/design/parent_theme.dart';
import 'package:flutter_parent/utils/design/theme_transition/theme_transition_target.dart';
@@ -28,6 +29,10 @@ class SettingsInteractor {
void routeToThemeViewer(BuildContext context) {
locator().push(context, ThemeViewerScreen());
}
+
+ void routeToRemoteConfig(BuildContext context) {
+ locator().push(context, RemoteConfigScreen());
+ }
void toggleDarkMode(context, anchorKey) {
if (ParentTheme.of(context).isDarkMode) {
diff --git a/apps/flutter_parent/lib/screens/settings/settings_screen.dart b/apps/flutter_parent/lib/screens/settings/settings_screen.dart
index 8b6256c314..6efb9f81bc 100644
--- a/apps/flutter_parent/lib/screens/settings/settings_screen.dart
+++ b/apps/flutter_parent/lib/screens/settings/settings_screen.dart
@@ -55,6 +55,7 @@ class _SettingsScreenState extends State {
if (ParentTheme.of(context).isDarkMode) _webViewDarkModeSwitch(context),
_highContrastModeSwitch(context),
if (_interactor.isDebugMode()) _themeViewer(context),
+ if (_interactor.isDebugMode()) _remoteConfigs(context)
],
),
),
@@ -193,6 +194,19 @@ class _SettingsScreenState extends State {
onTap: () => _interactor.routeToThemeViewer(context),
);
+ Widget _remoteConfigs(BuildContext context) =>
+ ListTile(
+ key: Key('remote-configs'),
+ title: Row(
+ children: [
+ _debugLabel(context),
+ SizedBox(width: 16),
+ Text('Remote Config Params')
+ ],
+ ),
+ onTap: () => _interactor.routeToRemoteConfig(context),
+ );
+
Container _debugLabel(BuildContext context) {
return Container(
decoration: BoxDecoration(
@@ -200,7 +214,8 @@ class _SettingsScreenState extends State {
borderRadius: BorderRadius.circular(32),
),
padding: const EdgeInsets.all(4),
- child: Icon(Icons.bug_report, color: Theme.of(context).accentIconTheme.color, size: 16),
+ child: Icon(Icons.bug_report,
+ color: Theme.of(context).accentIconTheme.color, size: 16),
);
}
}
diff --git a/apps/flutter_parent/lib/utils/remote_config_utils.dart b/apps/flutter_parent/lib/utils/remote_config_utils.dart
index 092f58f6d0..864c48200e 100644
--- a/apps/flutter_parent/lib/utils/remote_config_utils.dart
+++ b/apps/flutter_parent/lib/utils/remote_config_utils.dart
@@ -50,7 +50,8 @@ class RemoteConfigUtils {
*/
@visibleForTesting
static Future initializeExplicit(RemoteConfig remoteConfig) async {
- if (_remoteConfig != null) throw StateError('double-initialization of RemoteConfigUtils');
+ if (_remoteConfig != null)
+ throw StateError('double-initialization of RemoteConfigUtils');
_remoteConfig = remoteConfig;
@@ -70,7 +71,7 @@ class RemoteConfigUtils {
if (updated) {
// If we actually fetched something, then store the fetched info into _prefs
RemoteConfigParams.values.forEach((rc) {
- String rcParamName = _getRemoteConfigName(rc);
+ String rcParamName = getRemoteConfigName(rc);
String rcParamValue = _remoteConfig.getString(rcParamName);
String rcPreferencesName = _getSharedPreferencesName(rc);
print(
@@ -82,7 +83,7 @@ class RemoteConfigUtils {
// a local remote-config settings page, which is not supported at this time.
print('RemoteConfigUtils.initialize(): No update');
RemoteConfigParams.values.forEach((rc) {
- String rcParamName = _getRemoteConfigName(rc);
+ String rcParamName = getRemoteConfigName(rc);
String rcPreferencesName = _getSharedPreferencesName(rc);
String rcParamValue = _prefs.getString(rcPreferencesName);
print(
@@ -93,7 +94,8 @@ class RemoteConfigUtils {
/** Fetch the value (in string form) of the specified RemoteConfigParams element. */
static String getStringValue(RemoteConfigParams rcParam) {
- if (_remoteConfig == null) throw StateError('RemoteConfigUtils not yet initialized');
+ if (_remoteConfig == null)
+ throw StateError('RemoteConfigUtils not yet initialized');
var rcDefault = _getRemoteConfigDefaultValue(rcParam);
var rcPreferencesName = _getSharedPreferencesName(rcParam);
@@ -111,7 +113,7 @@ class RemoteConfigUtils {
// Switch statements are required to cover all possible cases, so if we add
// a new element in RemoveConfigParams, we'll be forced to add handling for
// it here.
- static String _getRemoteConfigName(RemoteConfigParams rcParam) {
+ static String getRemoteConfigName(RemoteConfigParams rcParam) {
switch (rcParam) {
case RemoteConfigParams.TEST_STRING:
return 'test_string';
@@ -141,6 +143,10 @@ class RemoteConfigUtils {
// that corresponds to rcParam. Just prepends an 'rc_' to the
// remote config name for rcParam.
static String _getSharedPreferencesName(RemoteConfigParams rcParam) {
- return 'rc_${_getRemoteConfigName(rcParam)}';
+ return 'rc_${getRemoteConfigName(rcParam)}';
+ }
+
+ static void updateRemoteConfig(RemoteConfigParams rcParam, String newValue) {
+ _prefs.setString(_getSharedPreferencesName(rcParam), newValue);
}
}
diff --git a/apps/flutter_parent/lib/utils/service_locator.dart b/apps/flutter_parent/lib/utils/service_locator.dart
index d66b6f5736..1588671191 100644
--- a/apps/flutter_parent/lib/utils/service_locator.dart
+++ b/apps/flutter_parent/lib/utils/service_locator.dart
@@ -58,6 +58,7 @@ import 'package:flutter_parent/screens/pairing/pairing_interactor.dart';
import 'package:flutter_parent/screens/pairing/pairing_util.dart';
import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen_interactor.dart';
import 'package:flutter_parent/screens/qr_login/qr_login_util.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart';
import 'package:flutter_parent/screens/settings/settings_interactor.dart';
import 'package:flutter_parent/screens/splash/splash_screen_interactor.dart';
import 'package:flutter_parent/screens/web_login/web_login_interactor.dart';
@@ -137,6 +138,7 @@ void setupLocator() {
locator.registerFactory(() => MasqueradeScreenInteractor());
locator.registerFactory(() => PairingInteractor());
locator.registerFactory(() => QRLoginTutorialScreenInteractor());
+ locator.registerFactory(() => RemoteConfigInteractor());
locator.registerFactory(() => SettingsInteractor());
locator.registerFactory(() => SplashScreenInteractor());
locator.registerFactory(() => StudentColorPickerInteractor());
diff --git a/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart b/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart
new file mode 100644
index 0000000000..04be99ec7a
--- /dev/null
+++ b/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 - present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+import 'package:flutter/widgets.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart';
+import 'package:flutter_parent/utils/remote_config_utils.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../utils/platform_config.dart';
+import '../../utils/test_app.dart';
+import '../../utils/test_helpers/mock_helpers.dart';
+
+void main() {
+ test('getRemoteConfigParams returns correct map', () async {
+ final mockRemoteConfig = setupMockRemoteConfig(valueSettings: {'test_string': 'fetched value', 'mobile_verify_beta_enabled' : 'false'});
+ await setupPlatformChannels(
+ config: PlatformConfig(initRemoteConfig: mockRemoteConfig));
+ expect(RemoteConfigInteractor().getRemoteConfigParams(), equals({RemoteConfigParams.TEST_STRING: 'fetched value', RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED: 'false'}));
+ });
+}
\ No newline at end of file
diff --git a/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart
new file mode 100644
index 0000000000..0c9756e102
--- /dev/null
+++ b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart
@@ -0,0 +1,50 @@
+// Copyright (C) 2020 - present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart';
+import 'package:flutter_parent/utils/remote_config_utils.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../utils/accessibility_utils.dart';
+import '../../utils/platform_config.dart';
+import '../../utils/test_app.dart';
+import '../../utils/test_helpers/mock_helpers.dart';
+
+void main() {
+
+ testWidgetsWithAccessibilityChecks('Shows the correct list', (tester) async {
+ var interactor = _MockInteractor();
+ setupTestLocator((locator) => locator.registerFactory(() => interactor));
+
+ Map remoteConfigs = {
+ RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED: 'false',
+ RemoteConfigParams.TEST_STRING: 'fetched value'
+ };
+
+ when(interactor.getRemoteConfigParams()).thenReturn(remoteConfigs);
+
+ await tester.pumpWidget(TestApp(RemoteConfigScreen()));
+ await tester.pumpAndSettle();
+
+ expect(find.text(RemoteConfigUtils.getRemoteConfigName(RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED)), findsOneWidget);
+ expect(find.text('false'), findsOneWidget);
+ expect(find.text(RemoteConfigUtils.getRemoteConfigName(RemoteConfigParams.TEST_STRING)), findsOneWidget);
+ expect(find.text('fetched value'), findsOneWidget);
+ });
+}
+
+class _MockInteractor extends Mock implements RemoteConfigInteractor {}
diff --git a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart
index 8dcd6df7f1..a043798e18 100644
--- a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart
+++ b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart
@@ -15,6 +15,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_parent/network/utils/analytics.dart';
+import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart';
import 'package:flutter_parent/screens/settings/settings_interactor.dart';
import 'package:flutter_parent/screens/theme_viewer_screen.dart';
import 'package:flutter_parent/utils/design/parent_theme.dart';
@@ -44,6 +45,19 @@ void main() {
expect(screen, isA());
});
+ test('routeToRemoteConfig call through to navigator', () {
+ var nav = _MockNav();
+ setupTestLocator((locator) {
+ locator.registerLazySingleton(() => nav);
+ });
+
+ var context = _MockContext();
+ SettingsInteractor().routeToRemoteConfig(context);
+
+ var screen = verify(nav.push(context, captureAny)).captured[0];
+ expect(screen, isA());
+ });
+
testNonWidgetsWithContext('toggle dark mode sets dark mode to true', (tester) async {
await setupPlatformChannels();
final analytics = _MockAnalytics();
diff --git a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart
index cf4f849718..59fadba746 100644
--- a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart
+++ b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart
@@ -32,6 +32,7 @@ void main() {
AppLocalizations l10n = AppLocalizations();
themeViewerButton() => find.byKey(Key('theme-viewer'));
+ remoteConfigsButton() => find.byKey(Key('remote-configs'));
darkModeButton() => find.byKey(Key('dark-mode-button'));
lightModeButton() => find.byKey(Key('light-mode-button'));
hcToggle() => find.text(l10n.highContrastLabel);
@@ -64,6 +65,14 @@ void main() {
expect(themeViewerButton(), findsOneWidget);
});
+ testWidgetsWithAccessibilityChecks('Displays remote config params button in debug mode', (tester) async {
+ await tester.pumpWidget(TestApp(SettingsScreen()));
+ await tester.pumpAndSettle();
+ await ensureVisibleByScrolling(themeViewerButton(), tester, scrollFrom: ScreenVerticalLocation.MID_BOTTOM);
+ await tester.pumpAndSettle();
+ expect(remoteConfigsButton(), findsOneWidget);
+ });
+
testWidgetsWithAccessibilityChecks('Hides theme viewer button in non-debug mode', (tester) async {
when(interactor.isDebugMode()).thenReturn(false);
await tester.pumpWidget(TestApp(SettingsScreen()));
@@ -71,6 +80,13 @@ void main() {
expect(themeViewerButton(), findsNothing);
});
+ testWidgetsWithAccessibilityChecks('Hide remote config params button in non-debug mode', (tester) async {
+ when(interactor.isDebugMode()).thenReturn(false);
+ await tester.pumpWidget(TestApp(SettingsScreen()));
+ await tester.pumpAndSettle();
+ expect(remoteConfigsButton(), findsNothing);
+ });
+
testWidgetsWithAccessibilityChecks(
'(In light mode) Dark mode button is enabled, light mode button is disabled',
(tester) async {
diff --git a/apps/flutter_parent/test/utils/remote_config_utils_test.dart b/apps/flutter_parent/test/utils/remote_config_utils_test.dart
index db7f5a78e1..069c689caa 100644
--- a/apps/flutter_parent/test/utils/remote_config_utils_test.dart
+++ b/apps/flutter_parent/test/utils/remote_config_utils_test.dart
@@ -70,4 +70,15 @@ void main() {
expect(RemoteConfigUtils.getStringValue(RemoteConfigParams.TEST_STRING), 'fetched value');
});
+
+ test('update cached value', () async {
+ final mockRemoteConfig = setupMockRemoteConfig();
+ final platformConfig =
+ PlatformConfig(mockPrefs: {'rc_test_string': 'cached value'}, initRemoteConfig: mockRemoteConfig);
+ await setupPlatformChannels(config: platformConfig);
+
+ RemoteConfigUtils.updateRemoteConfig(RemoteConfigParams.TEST_STRING, 'updated cached value');
+
+ expect(RemoteConfigUtils.getStringValue(RemoteConfigParams.TEST_STRING), 'updated cached value');
+ });
}