Skip to content

Commit

Permalink
[MBL-14971][Parent] Manage remote config flags (#1128)
Browse files Browse the repository at this point in the history
* [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.
  • Loading branch information
hermannakos authored Dec 2, 2020
1 parent 1566a76 commit 25a6034
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

import 'package:flutter_parent/utils/remote_config_utils.dart';

class RemoteConfigInteractor {

Map<RemoteConfigParams, String> getRemoteConfigParams() {
return Map.fromIterable(RemoteConfigParams.values, key: (rc) => rc, value: (rc) => RemoteConfigUtils.getStringValue(rc));
}

void updateRemoteConfig(RemoteConfigParams rcKey, String value) {
RemoteConfigUtils.updateRemoteConfig(rcKey, value);
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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<RemoteConfigScreen> {
RemoteConfigInteractor _interactor = locator<RemoteConfigInteractor>();

Map<RemoteConfigParams, String> _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<Widget> _createListItems(Map<RemoteConfigParams, String> remoteConfigParams) {
return remoteConfigParams.entries.map((e) => _createListItem(e)).toList();
}

Widget _createListItem(MapEntry<RemoteConfigParams, String> 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)
},
),
))
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,10 @@ class SettingsInteractor {
void routeToThemeViewer(BuildContext context) {
locator<QuickNav>().push(context, ThemeViewerScreen());
}

void routeToRemoteConfig(BuildContext context) {
locator<QuickNav>().push(context, RemoteConfigScreen());
}

void toggleDarkMode(context, anchorKey) {
if (ParentTheme.of(context).isDarkMode) {
Expand Down
17 changes: 16 additions & 1 deletion apps/flutter_parent/lib/screens/settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (ParentTheme.of(context).isDarkMode) _webViewDarkModeSwitch(context),
_highContrastModeSwitch(context),
if (_interactor.isDebugMode()) _themeViewer(context),
if (_interactor.isDebugMode()) _remoteConfigs(context)
],
),
),
Expand Down Expand Up @@ -193,14 +194,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(
color: Theme.of(context).accentColor,
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),
);
}
}
18 changes: 12 additions & 6 deletions apps/flutter_parent/lib/utils/remote_config_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class RemoteConfigUtils {
*/
@visibleForTesting
static Future<void> initializeExplicit(RemoteConfig remoteConfig) async {
if (_remoteConfig != null) throw StateError('double-initialization of RemoteConfigUtils');
if (_remoteConfig != null)
throw StateError('double-initialization of RemoteConfigUtils');

_remoteConfig = remoteConfig;

Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -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';
Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions apps/flutter_parent/lib/utils/service_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -137,6 +138,7 @@ void setupLocator() {
locator.registerFactory<MasqueradeScreenInteractor>(() => MasqueradeScreenInteractor());
locator.registerFactory<PairingInteractor>(() => PairingInteractor());
locator.registerFactory<QRLoginTutorialScreenInteractor>(() => QRLoginTutorialScreenInteractor());
locator.registerFactory<RemoteConfigInteractor>(() => RemoteConfigInteractor());
locator.registerFactory<SettingsInteractor>(() => SettingsInteractor());
locator.registerFactory<SplashScreenInteractor>(() => SplashScreenInteractor());
locator.registerFactory<StudentColorPickerInteractor>(() => StudentColorPickerInteractor());
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
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'}));
});
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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<RemoteConfigInteractor>(() => interactor));

Map<RemoteConfigParams, String> 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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,19 @@ void main() {
expect(screen, isA<ThemeViewerScreen>());
});

test('routeToRemoteConfig call through to navigator', () {
var nav = _MockNav();
setupTestLocator((locator) {
locator.registerLazySingleton<QuickNav>(() => nav);
});

var context = _MockContext();
SettingsInteractor().routeToRemoteConfig(context);

var screen = verify(nav.push(context, captureAny)).captured[0];
expect(screen, isA<RemoteConfigScreen>());
});

testNonWidgetsWithContext('toggle dark mode sets dark mode to true', (tester) async {
await setupPlatformChannels();
final analytics = _MockAnalytics();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -64,13 +65,28 @@ 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()));
await tester.pumpAndSettle();
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 {
Expand Down
11 changes: 11 additions & 0 deletions apps/flutter_parent/test/utils/remote_config_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
}

0 comments on commit 25a6034

Please sign in to comment.