This commit is contained in:
Dain Nilsson 2023-03-06 10:43:12 +01:00
commit 69146be242
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
17 changed files with 569 additions and 481 deletions

View File

@ -15,27 +15,16 @@
*/ */
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:yubico_authenticator/android/init.dart'; import 'package:yubico_authenticator/android/init.dart';
import 'package:yubico_authenticator/android/keys.dart' as android_keys; import 'package:yubico_authenticator/android/keys.dart' as android_keys;
import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_view.dart'; import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_view.dart';
import 'package:yubico_authenticator/android/preferences.dart';
import 'package:yubico_authenticator/app/views/device_avatar.dart'; import 'package:yubico_authenticator/app/views/device_avatar.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys; import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import '../test_util.dart'; import '../test_util.dart';
void _setShowBetaDialogPref(bool value) async {
SharedPreferences.setMockInitialValues({betaDialogPrefName: value});
}
Future<void> startUp(WidgetTester tester, Future<void> startUp(WidgetTester tester,
[Map<dynamic, dynamic> startUpParams = const {}]) async { [Map<dynamic, dynamic> startUpParams = const {}]) async {
// on Android disable Beta welcome dialog
// we need to do it before we pump the app
var betaDlgEnabled = startUpParams['dlg.beta.enabled'] ?? false;
_setShowBetaDialogPref(betaDlgEnabled);
await tester.pumpWidget(await initialize()); await tester.pumpWidget(await initialize());
// only wait for yubikey connection when needed // only wait for yubikey connection when needed

View File

@ -16,27 +16,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'models.dart';
const _prefix = 'android.keys'; const _prefix = 'android.keys';
const betaDialogView = Key('$_prefix.beta_dialog');
const nfcTapSetting = Key('$_prefix.nfc_tap');
const nfcKeyboardLayoutSetting = Key('$_prefix.nfc_keyboard_layout');
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
const usbOpenApp = Key('$_prefix.usb_open_app');
const themeModeSetting = Key('$_prefix.theme_mode');
const okButton = Key('$_prefix.ok'); const okButton = Key('$_prefix.ok');
const manualEntryButton = Key('$_prefix.manual_entry'); const manualEntryButton = Key('$_prefix.manual_entry');
const launchTapAction = Key('$_prefix.tap_action_launch'); const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
const copyTapAction = Key('$_prefix.tap_action_copy'); const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
const bothTapAction = Key('$_prefix.tap_action_both'); const usbOpenApp = Key('$_prefix.usb_open_app');
const themeModeSystem = Key('$_prefix.theme_mode_system');
const themeModeLight = Key('$_prefix.theme_mode_light');
const themeModeDark = Key('$_prefix.theme_mode_dark');
const nfcTapSetting = Key('$_prefix.nfc_tap');
Key nfcTapOption(NfcTapAction action) =>
Key('$_prefix.tap_action.${action.name}');
const nfcKeyboardLayoutSetting = Key('$_prefix.nfc_keyboard_layout');
Key keyboardLayoutOption(String name) => Key('$_prefix.keyboard_layout.$name'); Key keyboardLayoutOption(String name) => Key('$_prefix.keyboard_layout.$name');

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 Yubico. * Copyright (C) 2023 Yubico.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
// shared preferences keys import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const betaDialogPrefName = 'prefBetaDialogShouldBeShown';
const prefNfcOpenApp = 'prefNfcOpenApp'; enum NfcTapAction {
const prefNfcBypassTouch = 'prefNfcBypassTouch'; launch,
const prefNfcSilenceSounds = 'prefNfcSilenceSounds'; copy,
const prefNfcCopyOtp = 'prefNfcCopyOtp'; both;
const prefClipKbdLayout = 'prefClipKbdLayout';
const prefUsbOpenApp = 'prefUsbOpenApp'; String getDescription(AppLocalizations l10n) {
const prefTheme = 'APP_STATE_THEME'; switch (this) {
case NfcTapAction.launch:
return l10n.l_launch_ya;
case NfcTapAction.copy:
return l10n.l_copy_otp_clipboard;
case NfcTapAction.both:
return l10n.l_launch_and_copy_otp;
}
}
}

View File

@ -17,11 +17,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../app/models.dart'; import '../app/models.dart';
import '../app/state.dart'; import '../app/state.dart';
import '../core/state.dart';
import 'app_methods.dart'; import 'app_methods.dart';
import 'devices.dart'; import 'devices.dart';
import 'models.dart';
const _contextChannel = MethodChannel('android.state.appContext'); const _contextChannel = MethodChannel('android.state.appContext');
@ -74,9 +77,8 @@ final androidSdkVersionProvider = Provider<int>((ref) => -1);
final androidNfcSupportProvider = Provider<bool>((ref) => false); final androidNfcSupportProvider = Provider<bool>((ref) => false);
final androidNfcStateProvider = StateNotifierProvider<NfcStateNotifier, bool>((ref) => final androidNfcStateProvider =
NfcStateNotifier() StateNotifierProvider<NfcStateNotifier, bool>((ref) => NfcStateNotifier());
);
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) { final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
if (ref.read(androidSdkVersionProvider) < 29) { if (ref.read(androidSdkVersionProvider) < 29) {
@ -124,3 +126,114 @@ class AndroidCurrentDeviceNotifier extends CurrentDeviceNotifier {
state = device; state = device;
} }
} }
final androidNfcTapActionProvider =
StateNotifierProvider<NfcTapActionNotifier, NfcTapAction>(
(ref) => NfcTapActionNotifier(ref.watch(prefProvider)));
class NfcTapActionNotifier extends StateNotifier<NfcTapAction> {
static const _prefNfcOpenApp = 'prefNfcOpenApp';
static const _prefNfcCopyOtp = 'prefNfcCopyOtp';
final SharedPreferences _prefs;
NfcTapActionNotifier._(this._prefs, super._state);
factory NfcTapActionNotifier(SharedPreferences prefs) {
final launchApp = prefs.getBool(_prefNfcOpenApp) ?? true;
final copyOtp = prefs.getBool(_prefNfcCopyOtp) ?? false;
final NfcTapAction action;
if (launchApp && copyOtp) {
action = NfcTapAction.both;
} else if (copyOtp) {
action = NfcTapAction.copy;
} else {
// This is the default value if both are false.
action = NfcTapAction.launch;
}
return NfcTapActionNotifier._(prefs, action);
}
Future<void> setTapAction(NfcTapAction value) async {
if (state != value) {
state = value;
await _prefs.setBool(_prefNfcOpenApp, value != NfcTapAction.copy);
await _prefs.setBool(_prefNfcCopyOtp, value != NfcTapAction.launch);
}
}
}
// TODO: Get these from Android
final androidNfcSupportedKbdLayoutsProvider =
Provider<List<String>>((ref) => ['US', 'DE', 'DE-CH']);
final androidNfcKbdLayoutProvider =
StateNotifierProvider<NfcKbdLayoutNotifier, String>(
(ref) => NfcKbdLayoutNotifier(ref.watch(prefProvider)));
class NfcKbdLayoutNotifier extends StateNotifier<String> {
static const String _defaultClipKbdLayout = 'US';
static const _prefClipKbdLayout = 'prefClipKbdLayout';
final SharedPreferences _prefs;
NfcKbdLayoutNotifier(this._prefs)
: super(_prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout);
Future<void> setKeyboardLayout(String value) async {
if (state != value) {
state = value;
await _prefs.setString(_prefClipKbdLayout, value);
}
}
}
final androidNfcBypassTouchProvider =
StateNotifierProvider<NfcBypassTouchNotifier, bool>(
(ref) => NfcBypassTouchNotifier(ref.watch(prefProvider)));
class NfcBypassTouchNotifier extends StateNotifier<bool> {
static const _prefNfcBypassTouch = 'prefNfcBypassTouch';
final SharedPreferences _prefs;
NfcBypassTouchNotifier(this._prefs)
: super(_prefs.getBool(_prefNfcBypassTouch) ?? false);
Future<void> setNfcBypassTouch(bool value) async {
if (state != value) {
state = value;
await _prefs.setBool(_prefNfcBypassTouch, value);
}
}
}
final androidNfcSilenceSoundsProvider =
StateNotifierProvider<NfcSilenceSoundsNotifier, bool>(
(ref) => NfcSilenceSoundsNotifier(ref.watch(prefProvider)));
class NfcSilenceSoundsNotifier extends StateNotifier<bool> {
static const _prefNfcSilenceSounds = 'prefNfcSilenceSounds';
final SharedPreferences _prefs;
NfcSilenceSoundsNotifier(this._prefs)
: super(_prefs.getBool(_prefNfcSilenceSounds) ?? false);
Future<void> setNfcSilenceSounds(bool value) async {
if (state != value) {
state = value;
await _prefs.setBool(_prefNfcSilenceSounds, value);
}
}
}
final androidUsbLaunchAppProvider =
StateNotifierProvider<UsbLaunchAppNotifier, bool>(
(ref) => UsbLaunchAppNotifier(ref.watch(prefProvider)));
class UsbLaunchAppNotifier extends StateNotifier<bool> {
static const _prefUsbOpenApp = 'prefUsbOpenApp';
final SharedPreferences _prefs;
UsbLaunchAppNotifier(this._prefs)
: super(_prefs.getBool(_prefUsbOpenApp) ?? false);
Future<void> setUsbLaunchApp(bool value) async {
if (state != value) {
state = value;
await _prefs.setBool(_prefUsbOpenApp, value);
}
}
}

View File

@ -1,283 +0,0 @@
/*
* Copyright (C) 2022 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/state.dart';
import '../../core/state.dart';
import '../../widgets/list_title.dart';
import '../../widgets/responsive_dialog.dart';
import '../keys.dart' as keys;
import '../preferences.dart';
// TODO: Get these from Android
const List<String> _keyboardLayouts = ['US', 'DE', 'DE-CH'];
const String _defaultClipKbdLayout = 'US';
enum _TapAction {
launch,
copy,
both;
String getDescription(AppLocalizations l10n) {
switch (this) {
case _TapAction.launch:
return l10n.l_launch_ya;
case _TapAction.copy:
return l10n.l_copy_otp_clipboard;
case _TapAction.both:
return l10n.l_launch_and_copy_otp;
}
}
Key get key {
switch (this) {
case _TapAction.launch:
return keys.launchTapAction;
case _TapAction.copy:
return keys.copyTapAction;
case _TapAction.both:
return keys.bothTapAction;
}
}
static _TapAction load(SharedPreferences prefs) {
final launchApp = prefs.getBool(prefNfcOpenApp) ?? true;
final copyOtp = prefs.getBool(prefNfcCopyOtp) ?? false;
if (launchApp && copyOtp) {
return both;
}
if (copyOtp) {
return copy;
}
// This is the default value if both are false.
return launch;
}
void save(SharedPreferences prefs) {
prefs.setBool(prefNfcOpenApp, this != copy);
prefs.setBool(prefNfcCopyOtp, this != launch);
}
}
extension on ThemeMode {
String getDisplayName(AppLocalizations l10n) {
switch (this) {
case ThemeMode.system:
return l10n.s_system_default;
case ThemeMode.light:
return l10n.s_light_mode;
case ThemeMode.dark:
return l10n.s_dark_mode;
}
}
}
class AndroidSettingsPage extends ConsumerStatefulWidget {
const AndroidSettingsPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_AndroidSettingsPageState();
}
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final prefs = ref.watch(prefProvider);
final tapAction = _TapAction.load(prefs);
final clipKbdLayout =
prefs.getString(prefClipKbdLayout) ?? _defaultClipKbdLayout;
final nfcBypassTouch = prefs.getBool(prefNfcBypassTouch) ?? false;
final nfcSilenceSounds = prefs.getBool(prefNfcSilenceSounds) ?? false;
final usbOpenApp = prefs.getBool(prefUsbOpenApp) ?? false;
final themeMode = ref.watch(themeModeProvider);
final theme = Theme.of(context);
return ResponsiveDialog(
title: Text(l10n.s_settings),
child: Theme(
// Make the headers use the primary color to pop a bit.
// Once M3 is implemented this will probably not be needed.
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
labelLarge: theme.textTheme.labelLarge
?.copyWith(color: theme.colorScheme.primary)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTitle(l10n.s_nfc_options),
ListTile(
title: Text(l10n.l_on_yk_nfc_tap),
subtitle: Text(tapAction.getDescription(l10n)),
key: keys.nfcTapSetting,
onTap: () async {
final newTapAction = await _selectTapAction(context, tapAction);
setState(() {
newTapAction.save(prefs);
});
},
),
ListTile(
title: Text(l10n.l_kbd_layout_for_static),
subtitle: Text(clipKbdLayout),
key: keys.nfcKeyboardLayoutSetting,
enabled: tapAction != _TapAction.launch,
onTap: () async {
var newValue = await _selectKbdLayout(context, clipKbdLayout);
if (newValue != clipKbdLayout) {
setState(() {
prefs.setString(prefClipKbdLayout, newValue);
});
}
},
),
SwitchListTile(
title: Text(l10n.l_bypass_touch_requirement),
subtitle: Text(nfcBypassTouch
? l10n.l_bypass_touch_requirement_on
: l10n.l_bypass_touch_requirement_off),
value: nfcBypassTouch,
key: keys.nfcBypassTouchSetting,
onChanged: (value) {
setState(() {
prefs.setBool(prefNfcBypassTouch, value);
});
}),
SwitchListTile(
title: Text(l10n.s_silence_nfc_sounds),
subtitle: Text(nfcSilenceSounds
? l10n.l_silence_nfc_sounds_on
: l10n.l_silence_nfc_sounds_off),
value: nfcSilenceSounds,
key: keys.nfcSilenceSoundsSettings,
onChanged: (value) {
setState(() {
prefs.setBool(prefNfcSilenceSounds, value);
});
}),
ListTitle(l10n.s_usb_options),
SwitchListTile(
title: Text(l10n.l_launch_app_on_usb),
subtitle: Text(usbOpenApp
? l10n.l_launch_app_on_usb_on
: l10n.l_launch_app_on_usb_off),
value: usbOpenApp,
key: keys.usbOpenApp,
onChanged: (value) {
setState(() {
prefs.setBool(prefUsbOpenApp, value);
});
}),
ListTitle(l10n.s_appearance),
ListTile(
title: Text(l10n.s_app_theme),
subtitle: Text(themeMode.getDisplayName(l10n)),
key: keys.themeModeSetting,
onTap: () async {
final newMode = await _selectAppearance(
ref.read(supportedThemesProvider), context, themeMode);
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
},
),
],
),
),
);
}
Future<_TapAction> _selectTapAction(
BuildContext context, _TapAction tapAction) async =>
await showDialog<_TapAction>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.l_on_yk_nfc_tap),
children: _TapAction.values
.map(
(e) => RadioListTile<_TapAction>(
title: Text(e.getDescription(l10n)),
key: e.key,
value: e,
groupValue: tapAction,
toggleable: true,
onChanged: (mode) {
Navigator.pop(context, e);
}),
)
.toList(),
);
}) ??
_TapAction.launch;
Future<String> _selectKbdLayout(
BuildContext context, String currentKbdLayout) async =>
await showDialog<String>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.s_choose_kbd_layout),
children: _keyboardLayouts
.map(
(e) => RadioListTile<String>(
title: Text(e),
value: e,
key: keys.keyboardLayoutOption(e),
toggleable: true,
groupValue: currentKbdLayout,
onChanged: (mode) {
Navigator.pop(context, e);
}),
)
.toList(),
);
}) ??
_defaultClipKbdLayout;
Future<ThemeMode> _selectAppearance(List<ThemeMode> supportedThemes,
BuildContext context, ThemeMode themeMode) async =>
await showDialog<ThemeMode>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.s_choose_app_theme),
children: supportedThemes
.map((e) => RadioListTile(
title: Text(e.getDisplayName(l10n)),
value: e,
key: Key('android.keys.theme_mode_${e.name}'),
groupValue: themeMode,
toggleable: true,
onChanged: (mode) {
Navigator.pop(context, e);
},
))
.toList(),
);
}) ??
themeMode;
}

View File

@ -0,0 +1,187 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../state.dart';
import '../models.dart';
import '../keys.dart' as keys;
class NfcTapActionView extends ConsumerWidget {
const NfcTapActionView({super.key});
Future<NfcTapAction?> _selectTapAction(
BuildContext context, NfcTapAction tapAction) async =>
await showDialog<NfcTapAction>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.l_on_yk_nfc_tap),
children: NfcTapAction.values
.map(
(e) => RadioListTile<NfcTapAction>(
title: Text(e.getDescription(l10n)),
key: keys.nfcTapOption(e),
value: e,
groupValue: tapAction,
toggleable: true,
onChanged: (mode) {
Navigator.pop(context, e);
}),
)
.toList(),
);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final tapAction = ref.watch(androidNfcTapActionProvider);
return ListTile(
title: Text(l10n.l_on_yk_nfc_tap),
subtitle: Text(tapAction.getDescription(l10n)),
key: keys.nfcTapSetting,
onTap: () async {
final newTapAction = await _selectTapAction(context, tapAction);
if (newTapAction != null) {
await ref
.read(androidNfcTapActionProvider.notifier)
.setTapAction(newTapAction);
}
},
);
}
}
class NfcKbdLayoutView extends ConsumerWidget {
const NfcKbdLayoutView({super.key});
Future<String?> _selectKbdLayout(BuildContext context, List<String> available,
String currentKbdLayout) async =>
await showDialog<String>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.s_choose_kbd_layout),
children: available
.map(
(e) => RadioListTile<String>(
title: Text(e),
value: e,
key: keys.keyboardLayoutOption(e),
toggleable: true,
groupValue: currentKbdLayout,
onChanged: (mode) {
Navigator.pop(context, e);
}),
)
.toList(),
);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final tapAction = ref.watch(androidNfcTapActionProvider);
final clipKbdLayout = ref.watch(androidNfcKbdLayoutProvider);
return ListTile(
title: Text(l10n.l_kbd_layout_for_static),
subtitle: Text(clipKbdLayout),
key: keys.nfcKeyboardLayoutSetting,
enabled: tapAction != NfcTapAction.launch,
onTap: () async {
final newValue = await _selectKbdLayout(
context,
ref.watch(androidNfcSupportedKbdLayoutsProvider),
clipKbdLayout,
);
if (newValue != null) {
await ref
.read(androidNfcKbdLayoutProvider.notifier)
.setKeyboardLayout(newValue);
}
},
);
}
}
class NfcBypassTouchView extends ConsumerWidget {
const NfcBypassTouchView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final nfcBypassTouch = ref.watch(androidNfcBypassTouchProvider);
return SwitchListTile(
title: Text(l10n.l_bypass_touch_requirement),
subtitle: Text(nfcBypassTouch
? l10n.l_bypass_touch_requirement_on
: l10n.l_bypass_touch_requirement_off),
value: nfcBypassTouch,
key: keys.nfcBypassTouchSetting,
onChanged: (value) {
ref
.read(androidNfcBypassTouchProvider.notifier)
.setNfcBypassTouch(value);
});
}
}
class NfcSilenceSoundsView extends ConsumerWidget {
const NfcSilenceSoundsView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final nfcSilenceSounds = ref.watch(androidNfcSilenceSoundsProvider);
return SwitchListTile(
title: Text(l10n.s_silence_nfc_sounds),
subtitle: Text(nfcSilenceSounds
? l10n.l_silence_nfc_sounds_on
: l10n.l_silence_nfc_sounds_off),
value: nfcSilenceSounds,
key: keys.nfcSilenceSoundsSettings,
onChanged: (value) {
ref
.read(androidNfcSilenceSoundsProvider.notifier)
.setNfcSilenceSounds(value);
});
}
}
class UsbOpenAppView extends ConsumerWidget {
const UsbOpenAppView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final usbOpenApp = ref.watch(androidUsbLaunchAppProvider);
return SwitchListTile(
title: Text(l10n.l_launch_app_on_usb),
subtitle: Text(usbOpenApp
? l10n.l_launch_app_on_usb_on
: l10n.l_launch_app_on_usb_off),
value: usbOpenApp,
key: keys.usbOpenApp,
onChanged: (value) {
ref.read(androidUsbLaunchAppProvider.notifier).setUsbLaunchApp(value);
});
}
}

View File

@ -14,15 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:io';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
void launchFeedbackUrl() => _launchUrl(Platform.isAndroid import '../core/state.dart';
void launchFeedbackUrl() => _launchUrl(isAndroid
? 'https://yubi.co/ya-feedback-android' ? 'https://yubi.co/ya-feedback-android'
: 'https://yubi.co/ya-feedback-desktop'); : 'https://yubi.co/ya-feedback-desktop');
void launchHelpUrl() => _launchUrl(Platform.isAndroid void launchHelpUrl() => _launchUrl(isAndroid
? 'https://yubi.co/ya-help-android' ? 'https://yubi.co/ya-help-android'
: 'https://yubi.co/ya-help-desktop'); : 'https://yubi.co/ya-help-desktop');

View File

@ -22,14 +22,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../about_page.dart'; import '../about_page.dart';
import '../android/views/android_settings_page.dart';
import '../core/state.dart'; import '../core/state.dart';
import '../desktop/state.dart'; import '../desktop/state.dart';
import '../oath/keys.dart'; import '../oath/keys.dart';
import '../settings_page.dart';
import 'message.dart'; import 'message.dart';
import 'models.dart'; import 'models.dart';
import 'state.dart'; import 'state.dart';
import 'views/settings_page.dart';
class OpenIntent extends Intent { class OpenIntent extends Intent {
const OpenIntent(); const OpenIntent();
@ -122,9 +121,7 @@ Widget registerGlobalShortcuts(
if (!Navigator.of(context).canPop()) { if (!Navigator.of(context).canPop()) {
await showBlurDialog( await showBlurDialog(
context: context, context: context,
builder: (context) => Platform.isAndroid builder: (context) => const SettingsPage(),
? const AndroidSettingsPage()
: const SettingsPage(),
routeSettings: const RouteSettings(name: 'settings'), routeSettings: const RouteSettings(name: 'settings'),
); );
} }

View File

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -115,14 +113,13 @@ class _DevicePickerContent extends ConsumerWidget {
_HeroAvatar( _HeroAvatar(
child: DeviceAvatar( child: DeviceAvatar(
radius: 64, radius: 64,
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb), child: Icon(isAndroid ? Icons.no_cell : Icons.usb),
), ),
), ),
ListTile( ListTile(
title: Center(child: Text(l10n.l_no_yk_present)), title: Center(child: Text(l10n.l_no_yk_present)),
subtitle: Center( subtitle: Center(
child: Text( child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
Platform.isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
), ),
], ],
); );

View File

@ -26,3 +26,7 @@ const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
// drawer items // drawer items
const managementAppDrawer = Key('$_prefix.drawer.management'); const managementAppDrawer = Key('$_prefix.drawer.management');
// settings page
const themeModeSetting = Key('$_prefix.settings.theme_mode');
Key themeModeOption(ThemeMode mode) => Key('$_prefix.theme_mode.${mode.name}');

153
lib/app/views/settings_page.dart Executable file
View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../android/views/settings_views.dart';
import '../../core/state.dart';
import '../../widgets/list_title.dart';
import '../../widgets/responsive_dialog.dart';
import '../state.dart';
import 'keys.dart' as keys;
extension on ThemeMode {
String getDisplayName(AppLocalizations l10n) {
switch (this) {
case ThemeMode.system:
return l10n.s_system_default;
case ThemeMode.light:
return l10n.s_light_mode;
case ThemeMode.dark:
return l10n.s_dark_mode;
}
}
}
class _ThemeModeView extends ConsumerWidget {
const _ThemeModeView();
Future<ThemeMode> _selectAppearance(BuildContext context,
List<ThemeMode> supportedThemes, ThemeMode themeMode) async =>
await showDialog<ThemeMode>(
context: context,
builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog(
title: Text(l10n.s_choose_app_theme),
children: supportedThemes
.map((e) => RadioListTile(
title: Text(e.getDisplayName(l10n)),
value: e,
key: keys.themeModeOption(e),
groupValue: themeMode,
toggleable: true,
onChanged: (mode) {
Navigator.pop(context, e);
},
))
.toList(),
);
}) ??
themeMode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final themeMode = ref.watch(themeModeProvider);
return ListTile(
title: Text(l10n.s_app_theme),
subtitle: Text(themeMode.getDisplayName(l10n)),
key: keys.themeModeSetting,
onTap: () async {
final newMode = await _selectAppearance(
context, ref.read(supportedThemesProvider), themeMode);
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
},
);
}
}
class _CommunityTranslationsView extends ConsumerWidget {
const _CommunityTranslationsView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final enableTranslations = ref.watch(communityTranslationsProvider);
return SwitchListTile(
title: Text(l10n.l_enable_community_translations),
subtitle: Text(l10n.p_community_translations_desc),
isThreeLine: true,
value: enableTranslations,
onChanged: (value) {
ref
.read(communityTranslationsProvider.notifier)
.setEnableCommunityTranslations(value);
});
}
}
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final enableTranslations = ref.watch(communityTranslationsProvider);
return ResponsiveDialog(
title: Text(l10n.s_settings),
child: Theme(
// Make the headers use the primary color to pop a bit.
// Once M3 is implemented this will probably not be needed.
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
labelLarge: theme.textTheme.labelLarge
?.copyWith(color: theme.colorScheme.primary)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isAndroid) ...[
ListTitle(l10n.s_nfc_options),
const NfcTapActionView(),
const NfcKbdLayoutView(),
const NfcBypassTouchView(),
const NfcSilenceSoundsView(),
ListTitle(l10n.s_usb_options),
const UsbOpenAppView(),
],
ListTitle(l10n.s_appearance),
const _ThemeModeView(),
if (enableTranslations ||
basicLocaleListResolution(window.locales, officialLocales) !=
basicLocaleListResolution(
window.locales, AppLocalizations.supportedLocales)) ...[
ListTitle(l10n.s_language),
const _CommunityTranslationsView(),
],
],
),
),
);
}
}

View File

@ -14,14 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; bool get isDesktop {
final isAndroid = Platform.isAndroid; return const [
TargetPlatform.windows,
TargetPlatform.macOS,
TargetPlatform.linux
].contains(defaultTargetPlatform);
}
bool get isAndroid {
return defaultTargetPlatform == TargetPlatform.android;
}
// This must be initialized before use, in main.dart. // This must be initialized before use, in main.dart.
final prefProvider = Provider<SharedPreferences>((ref) { final prefProvider = Provider<SharedPreferences>((ref) {

View File

@ -56,7 +56,7 @@ final _favoriteAccounts =
); );
final systrayProvider = Provider.autoDispose((ref) { final systrayProvider = Provider.autoDispose((ref) {
final systray = _Systray(ref, ref.watch(l10nProvider)); final systray = _Systray(ref);
// Keep track of which accounts to show // Keep track of which accounts to show
ref.listen( ref.listen(
@ -64,6 +64,7 @@ final systrayProvider = Provider.autoDispose((ref) {
(_, next) { (_, next) {
systray._updateCredentials(next); systray._updateCredentials(next);
}, },
fireImmediately: true,
); );
// Keep track of the shown/hidden state of the app // Keep track of the shown/hidden state of the app
@ -71,6 +72,11 @@ final systrayProvider = Provider.autoDispose((ref) {
systray._setHidden(hidden); systray._setHidden(hidden);
}, fireImmediately: true); }, fireImmediately: true);
// Keep track of the locale of the app
ref.listen(l10nProvider, (_, l10n) {
systray._updateLocale(l10n);
});
ref.onDispose(systray.dispose); ref.onDispose(systray.dispose);
return systray; return systray;
@ -99,20 +105,17 @@ String _getIcon() {
class _Systray extends TrayListener { class _Systray extends TrayListener {
final Ref _ref; final Ref _ref;
final AppLocalizations _l10n;
int _lastClick = 0; int _lastClick = 0;
AppLocalizations _l10n;
DevicePath _devicePath = DevicePath([]); DevicePath _devicePath = DevicePath([]);
List<OathCredential> _credentials = []; List<OathCredential> _credentials = [];
bool _isHidden = false; bool _isHidden = false;
_Systray(this._ref, this._l10n) { _Systray(this._ref) : _l10n = _ref.read(l10nProvider) {
_init(); _init();
} }
Future<void> _init() async { Future<void> _init() async {
await trayManager.setIcon(_getIcon(), isTemplate: true); await trayManager.setIcon(_getIcon(), isTemplate: true);
if (!Platform.isLinux) {
await trayManager.setToolTip(_l10n.app_name);
}
await _updateContextMenu(); await _updateContextMenu();
// Doesn't seem to work on Linux // Doesn't seem to work on Linux
@ -120,9 +123,18 @@ class _Systray extends TrayListener {
} }
void dispose() { void dispose() {
trayManager.removeListener(this);
trayManager.destroy(); trayManager.destroy();
} }
void _updateLocale(AppLocalizations l10n) async {
_l10n = l10n;
if (!Platform.isLinux) {
await trayManager.setToolTip(l10n.app_name);
}
await _updateContextMenu();
}
void _updateCredentials(Pair<DevicePath?, List<OathCredential>> pair) { void _updateCredentials(Pair<DevicePath?, List<OathCredential>> pair) {
if (!listEquals(_credentials, pair.second)) { if (!listEquals(_credentials, pair.second)) {
_devicePath = pair.first ?? _devicePath; _devicePath = pair.first ?? _devicePath;

View File

@ -16,7 +16,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -177,7 +176,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
try { try {
if (devicePath == null) { if (devicePath == null) {
assert(Platform.isAndroid, 'devicePath is only optional for Android'); assert(isAndroid, 'devicePath is only optional for Android');
await ref await ref
.read(addCredentialToAnyProvider) .read(addCredentialToAnyProvider)
.call(credUri, requireTouch: _touch); .call(credUri, requireTouch: _touch);
@ -334,7 +333,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final devicePath = deviceNode?.path; final devicePath = deviceNode?.path;
if (devicePath != null) { if (devicePath != null) {
await _doAddCredential(devicePath: devicePath, credUri: cred.toUri()); await _doAddCredential(devicePath: devicePath, credUri: cred.toUri());
} else if (Platform.isAndroid) { } else if (isAndroid) {
// Send the credential to Android to be added to the next YubiKey // Send the credential to Android to be added to the next YubiKey
await _doAddCredential(devicePath: null, credUri: cred.toUri()); await _doAddCredential(devicePath: null, credUri: cred.toUri());
} else { } else {

View File

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -24,6 +22,7 @@ import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../core/state.dart';
import '../../exception/cancellation_exception.dart'; import '../../exception/cancellation_exception.dart';
import '../../widgets/list_title.dart'; import '../../widgets/list_title.dart';
import '../models.dart'; import '../models.dart';
@ -61,7 +60,7 @@ Widget oathBuildActions(
final withContext = ref.read(withContextProvider); final withContext = ref.read(withContextProvider);
Navigator.of(context).pop(); Navigator.of(context).pop();
CredentialData? otpauth; CredentialData? otpauth;
if (Platform.isAndroid) { if (isAndroid) {
final scanner = ref.read(qrScannerProvider); final scanner = ref.read(qrScannerProvider);
if (scanner != null) { if (scanner != null) {
try { try {

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2022 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'app/logging.dart';
import 'app/state.dart';
import 'widgets/list_title.dart';
import 'widgets/responsive_dialog.dart';
final _log = Logger('settings');
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final themeMode = ref.watch(themeModeProvider);
final theme = Theme.of(context);
final enableTranslations = ref.watch(communityTranslationsProvider);
return ResponsiveDialog(
title: Text(l10n.s_settings),
child: Theme(
// Make the headers use the primary color to pop a bit.
// Once M3 is implemented this will probably not be needed.
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
labelLarge: theme.textTheme.labelLarge
?.copyWith(color: theme.colorScheme.primary)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTitle(l10n.s_appearance),
RadioListTile<ThemeMode>(
title: Text(l10n.s_system_default),
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
RadioListTile<ThemeMode>(
title: Text(l10n.s_light_mode),
value: ThemeMode.light,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
RadioListTile<ThemeMode>(
title: Text(l10n.s_dark_mode),
value: ThemeMode.dark,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
if (enableTranslations ||
basicLocaleListResolution(window.locales, officialLocales) !=
basicLocaleListResolution(
window.locales, AppLocalizations.supportedLocales)) ...[
ListTitle(l10n.s_language),
SwitchListTile(
title: Text(l10n.l_enable_community_translations),
subtitle: Text(l10n.p_community_translations_desc),
isThreeLine: true,
value: enableTranslations,
onChanged: (value) {
ref
.read(communityTranslationsProvider.notifier)
.setEnableCommunityTranslations(value);
}),
],
],
),
),
);
}
}

View File

@ -15,16 +15,18 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:yubico_authenticator/android/keys.dart' as keys; import 'package:yubico_authenticator/android/models.dart';
import 'package:yubico_authenticator/android/preferences.dart'; import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/android/keys.dart' as android_keys;
import 'package:yubico_authenticator/android/state.dart'; import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/android/views/android_settings_page.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/app/views/settings_page.dart';
import 'package:yubico_authenticator/core/state.dart'; import 'package:yubico_authenticator/core/state.dart';
Widget createMaterialApp({required Widget child}) { Widget createMaterialApp({required Widget child}) {
@ -44,7 +46,7 @@ Widget createMaterialApp({required Widget child}) {
extension _WidgetTesterHelper on WidgetTester { extension _WidgetTesterHelper on WidgetTester {
Future<void> openNfcTapOptionSelection() async { Future<void> openNfcTapOptionSelection() async {
var widget = find.byKey(keys.nfcTapSetting).hitTestable(); var widget = find.byKey(android_keys.nfcTapSetting).hitTestable();
expect(widget, findsOneWidget); expect(widget, findsOneWidget);
await tap(widget); await tap(widget);
await pumpAndSettle(); await pumpAndSettle();
@ -52,28 +54,29 @@ extension _WidgetTesterHelper on WidgetTester {
Future<void> selectLaunchOption() async { Future<void> selectLaunchOption() async {
await openNfcTapOptionSelection(); await openNfcTapOptionSelection();
await tap(find.byKey(keys.launchTapAction)); await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launch)));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectCopyOption() async { Future<void> selectCopyOption() async {
await openNfcTapOptionSelection(); await openNfcTapOptionSelection();
await tap(find.byKey(keys.copyTapAction)); await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.copy)));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectBothOption() async { Future<void> selectBothOption() async {
await openNfcTapOptionSelection(); await openNfcTapOptionSelection();
await tap(find.byKey(keys.bothTapAction)); await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.both)));
await pumpAndSettle(); await pumpAndSettle();
} }
ListTile keyboardLayoutListTile() => ListTile keyboardLayoutListTile() =>
find.byKey(keys.nfcKeyboardLayoutSetting).evaluate().single.widget find.byKey(android_keys.nfcKeyboardLayoutSetting).evaluate().single.widget
as ListTile; as ListTile;
Future<void> openKeyboardLayoutOptionSelection() async { Future<void> openKeyboardLayoutOptionSelection() async {
var widget = find.byKey(keys.nfcKeyboardLayoutSetting).hitTestable(); var widget =
find.byKey(android_keys.nfcKeyboardLayoutSetting).hitTestable();
expect(widget, findsOneWidget); expect(widget, findsOneWidget);
await tap(widget); await tap(widget);
await pumpAndSettle(); await pumpAndSettle();
@ -81,44 +84,45 @@ extension _WidgetTesterHelper on WidgetTester {
Future<void> selectKeyboardLayoutUSOption() async { Future<void> selectKeyboardLayoutUSOption() async {
await openKeyboardLayoutOptionSelection(); await openKeyboardLayoutOptionSelection();
await tap(find.byKey(keys.keyboardLayoutOption('US'))); await tap(find.byKey(android_keys.keyboardLayoutOption('US')));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectKeyboardLayoutDEOption() async { Future<void> selectKeyboardLayoutDEOption() async {
await openKeyboardLayoutOptionSelection(); await openKeyboardLayoutOptionSelection();
await tap(find.byKey(keys.keyboardLayoutOption('DE'))); await tap(find.byKey(android_keys.keyboardLayoutOption('DE')));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectKeyboardLayoutDECHOption() async { Future<void> selectKeyboardLayoutDECHOption() async {
await openKeyboardLayoutOptionSelection(); await openKeyboardLayoutOptionSelection();
await tap(find.byKey(keys.keyboardLayoutOption('DE-CH'))); await tap(find.byKey(android_keys.keyboardLayoutOption('DE-CH')));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> tapBypassTouch() async { Future<void> tapBypassTouch() async {
await tap(find.byKey(keys.nfcBypassTouchSetting)); await tap(find.byKey(android_keys.nfcBypassTouchSetting));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> tapOpenAppOnUsb() async { Future<void> tapOpenAppOnUsb() async {
await ensureVisible(find.byKey(keys.usbOpenApp)); await ensureVisible(find.byKey(android_keys.usbOpenApp));
await tap(find.byKey(keys.usbOpenApp)); await tap(find.byKey(android_keys.usbOpenApp));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> tapSilenceNfcSounds() async { Future<void> tapSilenceNfcSounds() async {
await tap(find.byKey(keys.nfcSilenceSoundsSettings)); await tap(find.byKey(android_keys.nfcSilenceSoundsSettings));
await pumpAndSettle(); await pumpAndSettle();
} }
ListTile themeModeListTile() => ListTile themeModeListTile() =>
find.byKey(keys.themeModeSetting).evaluate().single.widget as ListTile; find.byKey(app_keys.themeModeSetting).evaluate().single.widget
as ListTile;
Future<void> openAppThemeOptionSelection() async { Future<void> openAppThemeOptionSelection() async {
await ensureVisible(find.byKey(keys.themeModeSetting)); await ensureVisible(find.byKey(app_keys.themeModeSetting));
var widget = find.byKey(keys.themeModeSetting).hitTestable(); var widget = find.byKey(app_keys.themeModeSetting).hitTestable();
expect(widget, findsOneWidget); expect(widget, findsOneWidget);
await tap(widget); await tap(widget);
await pumpAndSettle(); await pumpAndSettle();
@ -126,19 +130,19 @@ extension _WidgetTesterHelper on WidgetTester {
Future<void> selectSystemTheme() async { Future<void> selectSystemTheme() async {
await openAppThemeOptionSelection(); await openAppThemeOptionSelection();
await tap(find.byKey(keys.themeModeSystem)); await tap(find.byKey(app_keys.themeModeOption(ThemeMode.system)));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectLightTheme() async { Future<void> selectLightTheme() async {
await openAppThemeOptionSelection(); await openAppThemeOptionSelection();
await tap(find.byKey(keys.themeModeLight)); await tap(find.byKey(app_keys.themeModeOption(ThemeMode.light)));
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectDarkTheme() async { Future<void> selectDarkTheme() async {
await openAppThemeOptionSelection(); await openAppThemeOptionSelection();
await tap(find.byKey(keys.themeModeDark)); await tap(find.byKey(app_keys.themeModeOption(ThemeMode.dark)));
await pumpAndSettle(); await pumpAndSettle();
} }
} }
@ -156,9 +160,12 @@ Widget androidWidget({
], child: child); ], child: child);
void main() { void main() {
var widget = createMaterialApp(child: const AndroidSettingsPage()); debugDefaultTargetPlatformOverride = TargetPlatform.android;
var widget = createMaterialApp(child: const SettingsPage());
testWidgets('NFC Tap options', (WidgetTester tester) async { testWidgets('NFC Tap options', (WidgetTester tester) async {
const prefNfcOpenApp = 'prefNfcOpenApp';
const prefNfcCopyOtp = 'prefNfcCopyOtp';
SharedPreferences.setMockInitialValues( SharedPreferences.setMockInitialValues(
{prefNfcOpenApp: true, prefNfcCopyOtp: false}); {prefNfcOpenApp: true, prefNfcCopyOtp: false});
@ -191,6 +198,9 @@ void main() {
}); });
testWidgets('Static password keyboard layout', (WidgetTester tester) async { testWidgets('Static password keyboard layout', (WidgetTester tester) async {
const prefNfcOpenApp = 'prefNfcOpenApp';
const prefNfcCopyOtp = 'prefNfcCopyOtp';
const prefClipKbdLayout = 'prefClipKbdLayout';
SharedPreferences.setMockInitialValues( SharedPreferences.setMockInitialValues(
{prefNfcOpenApp: true, prefNfcCopyOtp: false, prefClipKbdLayout: 'US'}); {prefNfcOpenApp: true, prefNfcCopyOtp: false, prefClipKbdLayout: 'US'});
@ -229,6 +239,7 @@ void main() {
}); });
testWidgets('Bypass touch req', (WidgetTester tester) async { testWidgets('Bypass touch req', (WidgetTester tester) async {
const prefNfcBypassTouch = 'prefNfcBypassTouch';
SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false}); SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
@ -285,6 +296,7 @@ void main() {
// no value for theme // no value for theme
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
const prefTheme = 'APP_STATE_THEME';
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs, sharedPrefs: sharedPrefs,
@ -303,6 +315,7 @@ void main() {
}); });
testWidgets('Open app on USB', (WidgetTester tester) async { testWidgets('Open app on USB', (WidgetTester tester) async {
const prefUsbOpenApp = 'prefUsbOpenApp';
SharedPreferences.setMockInitialValues({prefUsbOpenApp: false}); SharedPreferences.setMockInitialValues({prefUsbOpenApp: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
@ -321,6 +334,7 @@ void main() {
}); });
testWidgets('Silence NFC sound', (WidgetTester tester) async { testWidgets('Silence NFC sound', (WidgetTester tester) async {
const prefNfcSilenceSounds = 'prefNfcSilenceSounds';
SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false}); SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
@ -337,4 +351,6 @@ void main() {
await tester.tapSilenceNfcSounds(); await tester.tapSilenceNfcSounds();
expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false)); expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false));
}); });
debugDefaultTargetPlatformOverride = null;
} }