Skip to content

Re-factor settings view #1935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6aa84ba
Move settings to its own section
elibon99 May 30, 2025
58bdad7
Skip `flutter test`
elibon99 Jun 16, 2025
ffb777f
Reformat l10n strings
elibon99 Jun 16, 2025
741fb63
Add reset confirm dialog and re-structure settings UI
elibon99 Jun 16, 2025
65c787a
Align toggle readers `SwitchListTile`
elibon99 Jun 24, 2025
7f1e02c
Collapse `NavigationItem` into more button
elibon99 Jun 26, 2025
c943803
Move toggle readers to general
elibon99 Jun 26, 2025
8301040
Avoid overflow errors
elibon99 Jun 27, 2025
a0c5a14
Remove left border in drawer mode
elibon99 Jun 27, 2025
982fe82
Ensure device picker and nav item are same height
elibon99 Jun 27, 2025
2194b0d
Don't use `keyActionsBuilder` in `HomeMessagePage`
elibon99 Jun 27, 2025
4002a5b
Allow switching to Settings when no YK is inserted
elibon99 Jun 27, 2025
0c68584
Ensure icon in navigation item stays in position
elibon99 Jun 27, 2025
28bd069
Move android settings test to integration tests
elibon99 Jun 30, 2025
527611e
Add allow screenshots to settings
elibon99 Jun 30, 2025
f0a8d2d
Fix navigation in drawer
elibon99 Jun 30, 2025
7579297
Add crowdin link and re-factor actions
elibon99 Aug 4, 2025
57ce812
Disable certain actions on Android
elibon99 Aug 4, 2025
33029da
Only show current section if enabled
elibon99 Aug 4, 2025
f158af2
Ensure current section is always visible
elibon99 Aug 5, 2025
e54df07
Show translation info on not fully proofread
elibon99 Aug 5, 2025
98cffbf
Adjust navigation height
elibon99 Aug 6, 2025
2013921
Fix navigation of sections in tests
elibon99 Aug 6, 2025
2792cdc
Do not save settings as last active view
elibon99 Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ class MainActivity : FlutterFragmentActivity() {

viewModel.appContext.observe(this) {
switchContextManager(it.appContext)
if (it.appContext != OperationContext.Home) {
if (it.appContext != OperationContext.Home && it.appContext != OperationContext.Settings) {
logger.debug("A YubiKey is connected, using it with the context {}", it.appContext)
viewModel.connectedYubiKey.value?.let(::launchProcessYubiKey)
}
Expand Down Expand Up @@ -541,6 +541,7 @@ class MainActivity : FlutterFragmentActivity() {
OperationContext.HsmAuth to homeContextManager,
OperationContext.OpenPgp to homeContextManager,
OperationContext.YubiOtp to homeContextManager,
OperationContext.Settings to homeContextManager,
)

contextManager = contextManagers[appPreferences.appContext]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ enum class OperationContext(val value: Int) {
FidoPasskeys(4),
YubiOtp(5),
Piv(6),
OpenPgp(7),
HsmAuth(8),
Management(9);
Settings(7),
OpenPgp(8),
HsmAuth(9),
Management(10);

companion object {
fun getByValue(value: Int) = entries.firstOrNull { it.value == value } ?: Default
Expand Down
267 changes: 218 additions & 49 deletions integration_test/keyless_test.dart
Original file line number Diff line number Diff line change
@@ -1,101 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:logging/logging.dart';
import 'package:patrol_finders/patrol_finders.dart';
import 'package:yubico_authenticator/android/models.dart';
import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/app/logging.dart';
import 'package:yubico_authenticator/app/models.dart';
import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/app/views/app_list_item.dart';
import 'package:yubico_authenticator/app/views/keys.dart';
import 'package:yubico_authenticator/app/views/settings_page.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/version.dart';

import 'utils.dart';

void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
Future<void> _selectNfcActionAndKeyboardLayout(
PatrolTester $,
NfcTapAction action,
String currentKeyboardLayout,
) async {
// Select NFC action
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<NfcTapAction> && widget.value == action,
),
).tap();

appGroup('Home', (params) {
testKeyless('Settings', params, ($) async {
await $.navigate(Section.home);
expect($.read(androidNfcTapActionProvider), action);

// Open settings dialog
await $.viewAction(settingDrawerIcon);
// Ensure keyboard layout is enabled
await $(nfcKeyboardLayoutSetting).tap();

// Change theme
await $(themeModeSetting).tap();
await $(themeModeOption(ThemeMode.dark)).tap();
expect($($.l10n.s_dark_mode), findsOneWidget);
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<String> &&
widget.value != currentKeyboardLayout,
),
).tap();
expect($.read(androidNfcKbdLayoutProvider), isNot(currentKeyboardLayout));

await $(themeModeSetting).tap();
await $(themeModeOption(ThemeMode.light)).tap();
expect($($.l10n.s_light_mode), findsOneWidget);
// Change back to original layout
await $(nfcKeyboardLayoutSetting).tap();
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<String> &&
widget.value == currentKeyboardLayout,
),
).tap();
expect($.read(androidNfcKbdLayoutProvider), currentKeyboardLayout);
}

await $(themeModeSetting).tap();
await $(themeModeOption(ThemeMode.system)).tap();
expect($($.l10n.s_system_default), findsOneWidget);
void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

appGroup('Settings', (params) {
testKeyless('General settings sections', params, ($) async {
await $.navigate(Section.settings);
final close = $(closeButton);

settingsSetion(SettingsSection section) => $(
$(AppListItem<SettingsSection>).which(
(widget) => (widget as AppListItem<SettingsSection>).item == section,
),
);

// Change language
final currentLocale = $.read(currentLocaleProvider);
await $(languageSetting).tap();
await $.selectOrOpenItem(settingsSetion(SettingsSection.language));
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<Locale> && widget.value != currentLocale,
),
).tap();
expect($.read(currentLocaleProvider), isNot(currentLocale));
if (close.exists) {
await close.tap();
}

// Change back to the original locale
await $(languageSetting).tap();
await $.selectOrOpenItem(settingsSetion(SettingsSection.language));
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<Locale> && widget.value == currentLocale,
),
).tap();
expect($.read(currentLocaleProvider), equals(currentLocale));
if (close.exists) {
await close.tap();
}

// Close the dialog
await $(closeButton).tap();
});

testKeyless('About', params, ($) async {
await $.navigate(Section.home);

// Open help dialog
await $.viewAction(helpDrawerIcon);
// Change theme
await $.selectOrOpenItem(settingsSetion(SettingsSection.theme));
await $(themeModeOption(ThemeMode.dark)).tap();
expect($($.l10n.s_dark_mode), findsAtLeast(1));
if (close.exists) {
await close.tap();
}

// Make sure version is visible
expect($(version), findsOneWidget);
await $.selectOrOpenItem(settingsSetion(SettingsSection.theme));
await $(themeModeOption(ThemeMode.light)).tap();
expect($($.l10n.s_light_mode), findsAtLeast(1));
if (close.exists) {
await close.tap();
}

// Test logging overlay warnings (not localized)
expect($(RegExp('WARNING:')), findsNothing);
await $.selectOrOpenItem(settingsSetion(SettingsSection.theme));
await $(themeModeOption(ThemeMode.system)).tap();
expect($($.l10n.s_system_default), findsAtLeast(1));
if (close.exists) {
await close.tap();
}

// Enable debug logging
await $(logLevelChip).tap();
await $('Debug').tap();
await $.selectOrOpenItem(settingsSetion(SettingsSection.debugging));
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<Level> && widget.value == Levels.DEBUG,
),
).tap();
expect($(RegExp('WARNING:')), findsOneWidget);
if (close.exists) {
await close.tap();
}

// Enable traffic logging
await $(logLevelChip).tap();
await $('Traffic').tap();
await $.selectOrOpenItem(settingsSetion(SettingsSection.debugging));
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<Level> && widget.value == Levels.TRAFFIC,
),
).tap();
expect($(RegExp('WARNING:.*logged')), findsOneWidget);

if (isAndroid) {
// Enable screenshots, and make sure both warnings are shown
await $(screenshotChip).tap();
expect($(RegExp('WARNING:.*logged.*screen')), findsOneWidget);
await $(screenshotChip).tap();
expect($(RegExp('WARNING:.*screen')), findsNothing);
expect($(RegExp('WARNING:.*logged')), findsOneWidget);
if (close.exists) {
await close.tap();
}

// Re-enable info logging
await $(logLevelChip).tap();
await $('Info').tap();
await $.selectOrOpenItem(settingsSetion(SettingsSection.debugging));
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<Level> && widget.value == Levels.INFO,
),
).tap();
expect($(RegExp('WARNING:')), findsNothing);
if (close.exists) {
await close.tap();
}

// Close the dialog
await $(closeButton).tap();
// Test help & about
await $.selectOrOpenItem(settingsSetion(SettingsSection.help));
// Make sure version is visible
expect($(version), findsOneWidget);
if (close.exists) {
await close.tap();
}
});

testKeyless('Android settings sections', params, ($) async {
await $.navigate(Section.settings);
final close = $(closeButton);

settingsSetion(SettingsSection section) => $(
$(AppListItem<SettingsSection>).which(
(widget) => (widget as AppListItem<SettingsSection>).item == section,
),
);

// Change on NFC tap action
final tapAction = $.read(androidNfcTapActionProvider);

await $.selectOrOpenItem(settingsSetion(SettingsSection.nfcAndUsb));

// Test no action
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<NfcTapAction> &&
widget.value == NfcTapAction.noAction,
),
).tap();
expect($.read(androidNfcTapActionProvider), NfcTapAction.noAction);

// Test OTP actions and keyboard layout
final currentKeyboardLayout = $.read(androidNfcKbdLayoutProvider);

// Test copy OTP to clipboard
await _selectNfcActionAndKeyboardLayout(
$,
NfcTapAction.copy,
currentKeyboardLayout,
);

// Test launch and copy OTP
await _selectNfcActionAndKeyboardLayout(
$,
NfcTapAction.launchAndCopy,
currentKeyboardLayout,
);

// Change back to the original action
await $(
find.byWidgetPredicate(
(widget) =>
widget is RadioListTile<NfcTapAction> &&
widget.value == tapAction,
),
).tap();
expect($.read(androidNfcTapActionProvider), tapAction);

// Change bypass touch requirement
final nfcBypassTouch = $.read(androidNfcBypassTouchProvider);
await $(nfcBypassTouchSetting).tap();
expect($.read(androidNfcBypassTouchProvider), !nfcBypassTouch);

// Change back to default touch requirement
await $(nfcBypassTouchSetting).tap();
expect($.read(androidNfcBypassTouchProvider), nfcBypassTouch);

// Change silence NFC sounds
final nfcSilenceSounds = $.read(androidNfcSilenceSoundsProvider);
await $(nfcSilenceSoundsSettings).tap();
expect($.read(androidNfcSilenceSoundsProvider), !nfcSilenceSounds);

// Change back to default silence NFC sounds
await $(nfcSilenceSoundsSettings).tap();
expect($.read(androidNfcSilenceSoundsProvider), nfcSilenceSounds);

// Change on USB insert
final usbOpenApp = $.read(androidUsbLaunchAppProvider);
await $(usbOpenAppSetting).tap();
expect($.read(androidUsbLaunchAppProvider), !usbOpenApp);

// Change back to default on USB insert
await $(usbOpenAppSetting).tap();
expect($.read(androidUsbLaunchAppProvider), usbOpenApp);
if (close.exists) {
await close.tap();
}

// Change allow screenshots settings
final allowScreenshots = $.read(androidAllowScreenshotsProvider);
expect(allowScreenshots, false);

await $.selectOrOpenItem(settingsSetion(SettingsSection.debugging));
await $(allowScreenshotsSetting).tap();
expect($.read(androidAllowScreenshotsProvider), true);
expect($(RegExp('WARNING:.*record')), findsOneWidget);

// Change back to default allow screenshots
await $(allowScreenshotsSetting).tap();
expect($.read(androidAllowScreenshotsProvider), allowScreenshots);

if (close.exists) {
await close.tap();
}
}, skip: !isAndroid);
});
}
3 changes: 2 additions & 1 deletion integration_test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,8 @@ extension PatrolTesterUtils on PatrolTester {
await $(drawerIconButtonKey).tap();
}

await targetButton.scrollTo().tap();
// TODO: Need to take care of sections that are hidden in the more button
await targetButton.tap();

// Android may need some extra time to settle after navigation
if (isAndroid) {
Expand Down
Loading
Loading