From 921190ba406fea697758040d4416343a0d63fb39 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 15 Mar 2022 18:04:26 +0100 Subject: [PATCH 1/6] Introduce responsive dialogs. --- lib/about_page.dart | 69 ++- lib/app/views/main_drawer.dart | 19 +- lib/app/views/responsive_dialog.dart | 43 ++ lib/management/views/management_screen.dart | 226 +++++---- lib/oath/menu_actions.dart | 9 +- lib/oath/views/add_account_page.dart | 497 +++++++++----------- lib/oath/views/password_dialog.dart | 327 ++++++------- lib/oath/views/rename_account_dialog.dart | 29 +- lib/settings_page.dart | 78 ++- 9 files changed, 644 insertions(+), 653 deletions(-) create mode 100755 lib/app/views/responsive_dialog.dart diff --git a/lib/about_page.dart b/lib/about_page.dart index be4c60d2..ba957f9a 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; +import 'app/views/responsive_dialog.dart'; import 'core/state.dart'; import 'desktop/state.dart'; @@ -14,44 +15,36 @@ class AboutPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('About Yubico Authenticator'), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // TODO: Store the version number elsewhere - const Text('Yubico Authenticator version: 6.0.0-alpha.1'), - if (isDesktop) - Text('ykman version: ${ref.watch(rpcStateProvider).version}'), - Text('Dart version: ${Platform.version}'), - const SizedBox(height: 8.0), - const Divider(), - if (isDesktop) - TextButton( - onPressed: () async { - _log.info('Running diagnostics...'); - final response = - await ref.read(rpcProvider).command('diagnose', []); - _log.info('Response', response['diagnostics']); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: - Text('Diagnostics done. See log for results...'), - duration: Duration(seconds: 2), - ), - ); - }, - child: const Text('Run diagnostics...'), - ), - ], - ), - ), + return ResponsiveDialog( + title: const Text('About Yubico Authenticator'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: Store the version number elsewhere + const Text('Yubico Authenticator version: 6.0.0-alpha.1'), + if (isDesktop) + Text('ykman version: ${ref.watch(rpcStateProvider).version}'), + Text('Dart version: ${Platform.version}'), + const SizedBox(height: 8.0), + const Divider(), + if (isDesktop) + TextButton( + onPressed: () async { + _log.info('Running diagnostics...'); + final response = + await ref.read(rpcProvider).command('diagnose', []); + _log.info('Response', response['diagnostics']); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Diagnostics done. See log for results...'), + duration: Duration(seconds: 2), + ), + ); + }, + child: const Text('Run diagnostics...'), + ), + ], ), ); } diff --git a/lib/app/views/main_drawer.dart b/lib/app/views/main_drawer.dart index 0bddf5de..c2b228fb 100755 --- a/lib/app/views/main_drawer.dart +++ b/lib/app/views/main_drawer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../management/views/management_screen.dart'; import '../../about_page.dart'; import '../../settings_page.dart'; import '../models.dart'; @@ -78,12 +79,12 @@ class MainPageDrawer extends ConsumerWidget { DrawerItem( titleText: 'Toggle applications', icon: Icon(Application.management._icon), - selected: Application.management == currentApp, onTap: () { - ref - .read(currentAppProvider.notifier) - .setCurrentApp(Application.management); if (shouldPop) Navigator.of(context).pop(); + showDialog( + context: context, + builder: (context) => ManagementScreen(data), + ); }, ), const Divider(), @@ -103,9 +104,8 @@ class MainPageDrawer extends ConsumerWidget { onTap: () { final nav = Navigator.of(context); if (shouldPop) nav.pop(); - nav.push( - MaterialPageRoute(builder: (context) => const SettingsPage()), - ); + showDialog( + context: context, builder: (context) => const SettingsPage()); }, ), DrawerItem( @@ -114,9 +114,8 @@ class MainPageDrawer extends ConsumerWidget { onTap: () { final nav = Navigator.of(context); if (shouldPop) nav.pop(); - nav.push( - MaterialPageRoute(builder: (context) => const AboutPage()), - ); + showDialog( + context: context, builder: (context) => const AboutPage()); }, ), ], diff --git a/lib/app/views/responsive_dialog.dart b/lib/app/views/responsive_dialog.dart new file mode 100755 index 00000000..7caa215b --- /dev/null +++ b/lib/app/views/responsive_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ResponsiveDialog extends StatelessWidget { + final Widget? title; + final Widget child; + final List? actions; + + const ResponsiveDialog( + {Key? key, required this.child, this.title, this.actions}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final query = MediaQuery.of(context); + if (query.size.width < 540) { + // Fullscreen + return Dialog( + insetPadding: const EdgeInsets.all(0), + child: Scaffold( + appBar: AppBar( + title: title, + actions: actions, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(18.0), + child: child, + ), + )); + } else { + // Dialog + return AlertDialog( + insetPadding: EdgeInsets.zero, + title: title, + scrollable: true, + content: SizedBox( + width: 380, + child: child, + ), + actions: actions, + ); + } + } +} diff --git a/lib/management/views/management_screen.dart b/lib/management/views/management_screen.dart index 8e173210..d169f1cd 100755 --- a/lib/management/views/management_screen.dart +++ b/lib/management/views/management_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:collection/collection.dart'; -import 'package:yubico_authenticator/app/views/app_loading_screen.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../app/views/app_failure_screen.dart'; +import '../../app/views/app_loading_screen.dart'; +import '../../app/views/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; @@ -92,37 +94,22 @@ class _ModeFormState extends State<_ModeForm> { } } -class _CapabilitiesForm extends StatefulWidget { - final DeviceInfo info; - final Function(Map) onSubmit; +class _CapabilitiesForm extends StatelessWidget { + final Map supported; + final Map enabled; + final Function(Map enabled) onChanged; - const _CapabilitiesForm(this.info, {required this.onSubmit, Key? key}) - : super(key: key); - - @override - State createState() => _CapabilitiesFormState(); -} - -class _CapabilitiesFormState extends State<_CapabilitiesForm> { - late Map _enabled; - - @override - void initState() { - super.initState(); - // Make sure to copy enabledCapabilites, not mutate the original. - _enabled = {...widget.info.config.enabledCapabilities}; - } + const _CapabilitiesForm({ + required this.onChanged, + required this.supported, + required this.enabled, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - final usbCapabilities = - widget.info.supportedCapabilities[Transport.usb] ?? 0; - final nfcCapabilities = - widget.info.supportedCapabilities[Transport.nfc] ?? 0; - - final changed = - !_mapEquals(widget.info.config.enabledCapabilities, _enabled); - final valid = changed && (_enabled[Transport.usb] ?? 0) > 0; + final usbCapabilities = supported[Transport.usb] ?? 0; + final nfcCapabilities = supported[Transport.nfc] ?? 0; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -134,11 +121,9 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> { ), _CapabilityForm( capabilities: usbCapabilities, - enabled: _enabled[Transport.usb] ?? 0, - onChanged: (enabled) { - setState(() { - _enabled[Transport.usb] = enabled; - }); + enabled: enabled[Transport.usb] ?? 0, + onChanged: (value) { + onChanged({...enabled, Transport.usb: value}); }, ), if (nfcCapabilities != 0) @@ -148,74 +133,87 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> { ), _CapabilityForm( capabilities: nfcCapabilities, - enabled: _enabled[Transport.nfc] ?? 0, - onChanged: (enabled) { - setState(() { - _enabled[Transport.nfc] = enabled; - }); + enabled: enabled[Transport.nfc] ?? 0, + onChanged: (value) { + onChanged({...enabled, Transport.nfc: value}); }, ), - Container( - padding: const EdgeInsets.all(16.0), - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: valid - ? () { - widget.onSubmit(_enabled); - } - : null, - child: const Text('Apply changes'), - ), - ) ], ); } } -class ManagementScreen extends ConsumerWidget { +class ManagementScreen extends ConsumerStatefulWidget { final YubiKeyData deviceData; const ManagementScreen(this.deviceData, {Key? key}) : super(key: key); - Widget _buildCapabilitiesForm( - BuildContext context, WidgetRef ref, DeviceInfo info) => - _CapabilitiesForm(info, onSubmit: (enabled) async { - final bool reboot; - if (deviceData.node is UsbYubiKeyNode) { - // Reboot if USB device descriptor is changed. - final oldInterfaces = UsbInterfaces.forCapabilites( - info.config.enabledCapabilities[Transport.usb] ?? 0); - final newInterfaces = - UsbInterfaces.forCapabilites(enabled[Transport.usb] ?? 0); - reboot = oldInterfaces != newInterfaces; - } else { - reboot = false; - } + @override + ConsumerState createState() => + _ManagementScreenState(); +} - Function()? close; - try { - if (reboot) { - // This will take longer, show a message - close = ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text('Reconfiguring YubiKey...'), - duration: Duration(seconds: 8), - )) - .close; - } - await ref - .read(managementStateProvider(deviceData.node.path).notifier) - .writeConfig( - info.config.copyWith(enabledCapabilities: enabled), - reboot: reboot, - ); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Configuration updated'), - duration: Duration(seconds: 2), - )); - } finally { - close?.call(); - } - }); +class _ManagementScreenState extends ConsumerState { + late Map _enabled; + + @override + void initState() { + super.initState(); + _enabled = widget.deviceData.info.config.enabledCapabilities; + } + + Widget _buildCapabilitiesForm( + BuildContext context, WidgetRef ref, DeviceInfo info) { + return _CapabilitiesForm( + supported: widget.deviceData.info.supportedCapabilities, + enabled: _enabled, + onChanged: (enabled) { + setState(() { + _enabled = enabled; + }); + }, + ); + } + + void _submitCapabilitiesForm() async { + final bool reboot; + if (widget.deviceData.node is UsbYubiKeyNode) { + // Reboot if USB device descriptor is changed. + final oldInterfaces = UsbInterfaces.forCapabilites( + widget.deviceData.info.config.enabledCapabilities[Transport.usb] ?? + 0); + final newInterfaces = + UsbInterfaces.forCapabilites(_enabled[Transport.usb] ?? 0); + reboot = oldInterfaces != newInterfaces; + } else { + reboot = false; + } + + Function()? close; + try { + if (reboot) { + // This will take longer, show a message + close = ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text('Reconfiguring YubiKey...'), + duration: Duration(seconds: 8), + )) + .close; + } + await ref + .read(managementStateProvider(widget.deviceData.node.path).notifier) + .writeConfig( + widget.deviceData.info.config + .copyWith(enabledCapabilities: _enabled), + reboot: reboot, + ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Configuration updated'), + duration: Duration(seconds: 2), + )); + } finally { + close?.call(); + } + } Widget _buildModeForm(BuildContext context, WidgetRef ref, DeviceInfo info) => _ModeForm( @@ -229,15 +227,41 @@ class ManagementScreen extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) => - ref.watch(managementStateProvider(deviceData.node.path)).when( - none: () => const AppLoadingScreen(), - failure: (reason) => AppFailureScreen(reason), - success: (info) => ListView( - children: [ - info.version.major > 4 - ? _buildCapabilitiesForm(context, ref, info) - : _buildModeForm(context, ref, info), - ], - )); + Widget build(BuildContext context) { + ref.listen(currentDeviceProvider, (_, __) { + //TODO: This can probably be checked better to make sure it's the main page. + Navigator.of(context).popUntil((route) => route.isFirst); + }); + + bool changed = false; + + return ResponsiveDialog( + title: const Text('Toggle applications'), + child: + ref.watch(managementStateProvider(widget.deviceData.node.path)).when( + none: () => const AppLoadingScreen(), + failure: (reason) => AppFailureScreen(reason), + success: (info) { + // TODO: Check mode for < YK5 intead + changed = !_mapEquals( + _enabled, + info.config.enabledCapabilities, + ); + return Column( + children: [ + info.version.major > 4 + ? _buildCapabilitiesForm(context, ref, info) + : _buildModeForm(context, ref, info), + ], + ); + }, + ), + actions: [ + TextButton( + onPressed: changed ? _submitCapabilitiesForm : null, + child: const Text('Save'), + ), + ], + ); + } } diff --git a/lib/oath/menu_actions.dart b/lib/oath/menu_actions.dart index 45434c29..d7e50029 100755 --- a/lib/oath/menu_actions.dart +++ b/lib/oath/menu_actions.dart @@ -19,11 +19,10 @@ List buildOathMenuActions(AutoDisposeProviderRef ref) { text: 'Add credential', icon: const Icon(Icons.add), action: (context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - OathAddAccountPage(device: device), - ), + showDialog( + context: context, + builder: (context) => + OathAddAccountPage(device: device), ); }, ), diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 383c2ac9..ded7d9e9 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -3,10 +3,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/oath/models.dart'; import '../../app/state.dart'; import '../../app/models.dart'; +import '../../app/views/responsive_dialog.dart'; +import '../models.dart'; import '../state.dart'; import 'utils.dart'; @@ -15,21 +16,21 @@ final _secretFormatterPattern = enum _QrScanState { none, scanning, success, failed } -class AddAccountForm extends ConsumerStatefulWidget { - final Function(CredentialData, bool) onSubmit; - const AddAccountForm({Key? key, required this.onSubmit}) : super(key: key); +class OathAddAccountPage extends ConsumerStatefulWidget { + const OathAddAccountPage({required this.device, Key? key}) : super(key: key); + final DeviceNode device; @override - ConsumerState createState() => _AddAccountFormState(); + ConsumerState createState() => + _OathAddAccountPageState(); } -class _AddAccountFormState extends ConsumerState { +class _OathAddAccountPageState extends ConsumerState { final _issuerController = TextEditingController(); final _accountController = TextEditingController(); final _secretController = TextEditingController(); final _periodController = TextEditingController(text: '$defaultPeriod'); bool _touch = false; - bool _advanced = false; OathType _oathType = defaultOathType; HashAlgorithm _hashAlgorithm = defaultHashAlgorithm; int _digits = defaultDigits; @@ -83,6 +84,12 @@ class _AddAccountFormState extends ConsumerState { @override Widget build(BuildContext context) { + // If current device changes, we need to pop back to the main Page. + ref.listen(currentDeviceProvider, (previous, next) { + //TODO: This can probably be checked better to make sure it's the main page. + Navigator.of(context).popUntil((route) => route.isFirst); + }); + final period = int.tryParse(_periodController.text) ?? -1; final remaining = getRemainingKeySpace( oathType: _oathType, @@ -100,277 +107,237 @@ class _AddAccountFormState extends ConsumerState { final qrScanner = ref.watch(qrScannerProvider); - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - controller: _issuerController, - autofocus: true, - enabled: issuerRemaining > 0, - maxLength: max(issuerRemaining, 1), - decoration: const InputDecoration( - labelText: 'Issuer (optional)', - helperText: - '', // Prevents dialog resizing when enabled = false - ), - onChanged: (value) { - setState(() { - // Update maxlengths - }); - }, - ), - TextField( - controller: _accountController, - maxLength: nameRemaining, - decoration: const InputDecoration( - labelText: 'Account name', - helperText: - '', // Prevents dialog resizing when enabled = false - ), - onChanged: (value) { - setState(() { - // Update maxlengths - }); - }, - ), - TextField( - controller: _secretController, - inputFormatters: [ - FilteringTextInputFormatter.allow(_secretFormatterPattern) + return ResponsiveDialog( + title: const Text('Add account'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Account details', + style: Theme.of(context).textTheme.headline6, + ), + TextField( + controller: _issuerController, + autofocus: true, + enabled: issuerRemaining > 0, + maxLength: max(issuerRemaining, 1), + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Issuer (optional)', + helperText: '', // Prevents dialog resizing when enabled = false + ), + onChanged: (value) { + setState(() { + // Update maxlengths + }); + }, + ), + TextField( + controller: _accountController, + maxLength: nameRemaining, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Account name', + helperText: '', // Prevents dialog resizing when enabled = false + ), + onChanged: (value) { + setState(() { + // Update maxlengths + }); + }, + ), + TextField( + controller: _secretController, + inputFormatters: [ + FilteringTextInputFormatter.allow(_secretFormatterPattern) + ], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Secret key', + errorText: _validateSecretLength && !secretLengthValid + ? 'Invalid length' + : null), + enabled: _qrState != _QrScanState.success, + onChanged: (value) { + setState(() { + _validateSecretLength = false; + }); + }, + ), + if (qrScanner != null) + Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Row( + children: [ + OutlinedButton.icon( + onPressed: () { + _scanQrCode(qrScanner); + }, + icon: const Icon(Icons.qr_code), + label: const Text('Scan QR code'), + ), + const SizedBox(width: 8.0), + ..._buildQrStatus(), ], - decoration: InputDecoration( - labelText: 'Secret key', - errorText: _validateSecretLength && !secretLengthValid - ? 'Invalid length' - : null), - enabled: _qrState != _QrScanState.success, - onChanged: (value) { + ), + ), + const Divider(), + Text( + 'Options', + style: Theme.of(context).textTheme.headline6, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + FilterChip( + label: const Text('Require touch'), + selected: _touch, + onSelected: (value) { setState(() { - _validateSecretLength = false; + _touch = value; }); }, ), - if (qrScanner != null) - Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Row( - children: [ - OutlinedButton.icon( - onPressed: () { - _scanQrCode(qrScanner); - }, - icon: const Icon(Icons.qr_code), - label: const Text('Scan QR code'), - ), - const SizedBox(width: 8.0), - ..._buildQrStatus(), - ], + Chip( + label: DropdownButtonHideUnderline( + child: DropdownButton( + value: _oathType, + isDense: true, + underline: null, + items: OathType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name.toUpperCase()), + )) + .toList(), + onChanged: _qrState != _QrScanState.success + ? (type) { + setState(() { + _oathType = type ?? OathType.totp; + }); + } + : null, ), ), + ), + Chip( + label: DropdownButtonHideUnderline( + child: DropdownButton( + value: _hashAlgorithm, + isDense: true, + underline: null, + items: HashAlgorithm.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name.toUpperCase()), + )) + .toList(), + onChanged: _qrState != _QrScanState.success + ? (type) { + setState(() { + _hashAlgorithm = type ?? HashAlgorithm.sha1; + }); + } + : null, + ), + ), + ), + if (_oathType == OathType.totp) + Chip( + label: DropdownButtonHideUnderline( + child: DropdownButton( + value: + int.tryParse(_periodController.text) ?? defaultPeriod, + isDense: true, + underline: null, + items: [20, 30, 45, 60] + .map((e) => DropdownMenuItem( + value: e, + child: Text('$e sec'), + )) + .toList(), + onChanged: _qrState != _QrScanState.success + ? (period) { + setState(() { + _periodController.text = + '${period ?? defaultPeriod}'; + }); + } + : null, + ), + ), + ), + Chip( + label: DropdownButtonHideUnderline( + child: DropdownButton( + value: _digits, + isDense: true, + underline: null, + items: [6, 7, 8] + .map((e) => DropdownMenuItem( + value: e, + child: Text('$e digits'), + )) + .toList(), + onChanged: _qrState != _QrScanState.success + ? (digits) { + setState(() { + _digits = digits ?? defaultDigits; + }); + } + : null, + ), + ), + ), ], ), - ), - CheckboxListTile( - title: const Text('Require touch'), - controlAffinity: ListTileControlAffinity.leading, - value: _touch, - onChanged: (value) { - setState(() { - _touch = value ?? false; - }); - }, - ), - CheckboxListTile( - title: const Text('Show advanced settings'), - controlAffinity: ListTileControlAffinity.leading, - value: _advanced, - onChanged: (value) { - setState(() { - _advanced = value ?? false; - }); - }, - ), - if (_advanced) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Type'), - value: _oathType, - items: OathType.values - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.name.toUpperCase()), - )) - .toList(), - onChanged: _qrState != _QrScanState.success - ? (type) { - setState(() { - _oathType = type ?? OathType.totp; - }); - } - : null, + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + actions: [ + TextButton( + onPressed: isValid + ? () { + if (secretLengthValid) { + final issuer = _issuerController.text; + + final cred = CredentialData( + issuer: issuer.isEmpty ? null : issuer, + name: _accountController.text, + secret: secret, + oathType: _oathType, + hashAlgorithm: _hashAlgorithm, + digits: _digits, + period: period, + ); + + ref + .read( + credentialListProvider(widget.device.path).notifier) + .addAccount(cred.toUri(), requireTouch: _touch); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Account added'), + duration: Duration(seconds: 2), ), - ), - const SizedBox( - width: 8.0, - ), - Expanded( - child: DropdownButtonFormField( - decoration: - const InputDecoration(labelText: 'Algorithm'), - value: _hashAlgorithm, - items: HashAlgorithm.values - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.name.toUpperCase()), - )) - .toList(), - onChanged: _qrState != _QrScanState.success - ? (type) { - setState(() { - _hashAlgorithm = type ?? HashAlgorithm.sha1; - }); - } - : null, - ), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_oathType == OathType.totp) - Expanded( - child: TextFormField( - controller: _periodController, - enabled: _qrState != _QrScanState.success, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - decoration: InputDecoration( - contentPadding: - // Manual alignment to match digits-dropdown. - const EdgeInsets.fromLTRB(0, 12, 0, 15), - labelText: 'Period', - errorText: - period > 0 ? null : 'Must be a positive number', - ), - onChanged: (value) { - setState(() { - // Update maxlengths - }); - }, - ), - ), - if (_oathType == OathType.totp) - const SizedBox( - width: 8.0, - ), - Expanded( - child: DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Digits'), - value: _digits, - items: [6, 7, 8] - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.toString()), - )) - .toList(), - onChanged: _qrState != _QrScanState.success - ? (value) { - setState(() { - _digits = value ?? defaultDigits; - }); - } - : null, - ), - ), - ], - ), - ], - ), - ), - Container( - padding: const EdgeInsets.all(16.0), - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: isValid - ? () { - if (secretLengthValid) { - final issuer = _issuerController.text; - widget.onSubmit( - CredentialData( - issuer: issuer.isEmpty ? null : issuer, - name: _accountController.text, - secret: secret, - oathType: _oathType, - hashAlgorithm: _hashAlgorithm, - digits: _digits, - period: period, - ), - _touch, - ); - } else { - setState(() { - _validateSecretLength = true; - }); - } + ); + } else { + setState(() { + _validateSecretLength = true; + }); } - : null, - child: const Text('Add account'), - ), + } + : null, + child: const Text('Save'), ), ], ); } } - -class OathAddAccountPage extends ConsumerWidget { - const OathAddAccountPage({required this.device, Key? key}) : super(key: key); - final DeviceNode device; - - @override - Widget build(BuildContext context, WidgetRef ref) { - // If current device changes, we need to pop back to the main Page. - ref.listen(currentDeviceProvider, (previous, next) { - //TODO: This can probably be checked better to make sure it's the main page. - Navigator.of(context).popUntil((route) => route.isFirst); - }); - - return Scaffold( - appBar: AppBar( - title: const Text('Add account'), - ), - body: ListView( - children: [ - AddAccountForm( - onSubmit: (cred, requireTouch) { - ref - .read(credentialListProvider(device.path).notifier) - .addAccount(cred.toUri(), requireTouch: requireTouch); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Account added'), - duration: Duration(seconds: 2), - ), - ); - }, - ) - ], - )); - } -} diff --git a/lib/oath/views/password_dialog.dart b/lib/oath/views/password_dialog.dart index bc88eeca..c7d83fd1 100755 --- a/lib/oath/views/password_dialog.dart +++ b/lib/oath/views/password_dialog.dart @@ -1,20 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/views/responsive_dialog.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../models.dart'; import '../state.dart'; -class ManagePasswordDialog extends ConsumerStatefulWidget { - final DeviceNode device; - const ManagePasswordDialog(this.device, {Key? key}) : super(key: key); +class _ManagePasswordForm extends ConsumerStatefulWidget { + final DevicePath path; + final OathState state; + final Function(bool)? onValid; + const _ManagePasswordForm(this.path, this.state, {this.onValid, Key? key}) + : super(key: key); @override ConsumerState createState() => - _ManagePasswordDialogState(); + _ManagePasswordFormState(); } -class _ManagePasswordDialogState extends ConsumerState { +class _ManagePasswordFormState extends ConsumerState<_ManagePasswordForm> { String _currentPassword = ''; String _newPassword = ''; String _confirmPassword = ''; @@ -22,185 +27,42 @@ class _ManagePasswordDialogState extends ConsumerState { @override Widget build(BuildContext context) { - // If current device changes, we need to pop back to the main Page. - ref.listen(currentDeviceProvider, (previous, next) { - Navigator.of(context).pop(); - }); - - return ref.watch(oathStateProvider(widget.device.path)).maybeWhen( - success: (state) => AlertDialog( - title: const Text('Manage password'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.hasKey) - Column( - children: [ - if (state.remembered) - // TODO: This is temporary, to be able to forget a password. - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: const [ - Text( - 'Your password is remembered by the app.', - style: - TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () async { - await ref - .read(oathStateProvider( - widget.device.path) - .notifier) - .forgetPassword(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text('Password forgotten'), - duration: Duration(seconds: 2), - ), - ); - }, - child: const Text('Forget'), - ), - ], - ), - ], - ), - ), - const Text( - 'Enter your current password to change it. If you don\'t know your password, you\'ll need to reset the YubiKey, then create a new password.'), - Row( - children: [ - Expanded( - child: TextField( - autofocus: true, - obscureText: true, - decoration: InputDecoration( - labelText: 'Current', - errorText: _currentIsWrong - ? 'Wrong password' - : null), - onChanged: (value) { - setState(() { - _currentPassword = value; - }); - }, - ), - ), - const SizedBox( - width: 8.0, - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - child: const Text('Remove'), - onPressed: _currentPassword.isNotEmpty - ? () async { - final result = await ref - .read(oathStateProvider( - widget.device.path) - .notifier) - .unsetPassword(_currentPassword); - if (result) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: - Text('Password removed'), - duration: Duration(seconds: 2), - ), - ); - } else { - setState(() { - _currentIsWrong = true; - }); - } - } - : null, - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 16.0, - ), - ], - ), - const Text( - 'Enter your new password. A password may contain letters, numbers and other characters.'), - Row( - children: [ - Expanded( - child: TextField( - autofocus: !state.hasKey, - obscureText: true, - decoration: - const InputDecoration(labelText: 'Password'), - onChanged: (value) { - setState(() { - _newPassword = value; - }); - }, - ), - ), - const SizedBox( - width: 8, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: const InputDecoration( - labelText: 'Confirm', - ), - onChanged: (value) { - setState(() { - _confirmPassword = value; - }); - }, - ), - ), - ], - ), - ], - ), - actions: [ + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.state.hasKey) ...[ + Text( + 'Current password', + style: Theme.of(context).textTheme.headline6, + ), + TextField( + autofocus: true, + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Current password', + errorText: _currentIsWrong ? 'Wrong password' : null), + onChanged: (value) { + setState(() { + _currentPassword = value; + }); + }, + ), + Wrap( + spacing: 8.0, + children: [ OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: _newPassword.isNotEmpty && - _newPassword == _confirmPassword && - (!state.hasKey || _currentPassword.isNotEmpty) + child: const Text('Remove password'), + onPressed: _currentPassword.isNotEmpty ? () async { final result = await ref - .read( - oathStateProvider(widget.device.path).notifier) - .setPassword(_currentPassword, _newPassword); + .read(oathStateProvider(widget.path).notifier) + .unsetPassword(_currentPassword); if (result) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Password set'), + content: Text('Password removed'), duration: Duration(seconds: 2), ), ); @@ -211,10 +73,117 @@ class _ManagePasswordDialogState extends ConsumerState { } } : null, - child: const Text('Save'), - ) + ), + if (widget.state.remembered) + OutlinedButton( + child: const Text('Clear saved password'), + onPressed: () async { + await ref + .read(oathStateProvider(widget.path).notifier) + .forgetPassword(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password forgotten'), + duration: Duration(seconds: 2), + ), + ); + }, + ), ], ), + const Divider(), + ], + Text( + 'New password', + style: Theme.of(context).textTheme.headline6, + ), + TextField( + autofocus: !widget.state.hasKey, + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'New password', + enabled: !widget.state.hasKey || _currentPassword.isNotEmpty, + ), + onChanged: (value) { + setState(() { + _newPassword = value; + }); + }, + ), + TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Confirm password', + enabled: _newPassword.isNotEmpty, + ), + onChanged: (value) { + setState(() { + _confirmPassword = value; + }); + }, + ), + Align( + alignment: Alignment.bottomRight, + child: ElevatedButton( + onPressed: _newPassword.isNotEmpty && + _newPassword == _confirmPassword && + (!widget.state.hasKey || _currentPassword.isNotEmpty) + ? () async { + final result = await ref + .read(oathStateProvider(widget.path).notifier) + .setPassword(_currentPassword, _newPassword); + if (result) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password set'), + duration: Duration(seconds: 2), + ), + ); + } else { + setState(() { + _currentIsWrong = true; + }); + } + } + : null, + child: const Text('Save'), + ), + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ); + } +} + +class ManagePasswordDialog extends ConsumerWidget { + final DeviceNode device; + const ManagePasswordDialog(this.device, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // If current device changes, we need to pop back to the main Page. + ref.listen(currentDeviceProvider, (previous, next) { + Navigator.of(context).pop(); + }); + + return ref.watch(oathStateProvider(device.path)).maybeWhen( + success: (state) => ResponsiveDialog( + title: const Text('Manage password'), + child: _ManagePasswordForm( + device.path, + state, + // Prevents from losing state on responsive change. + key: GlobalKey(), + ), + ), orElse: () { throw Exception( 'Attempted to show password dialog without an OathState'); diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 2f074bff..9a84b08c 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/views/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; import '../../app/models.dart'; @@ -53,11 +54,12 @@ class _RenameAccountDialogState extends ConsumerState { final nameRemaining = remaining.second; final isValid = _account.isNotEmpty; - return AlertDialog( - title: Text('Rename $label?'), - content: Column( - mainAxisSize: MainAxisSize.min, + return ResponsiveDialog( + title: const Text('Rename account'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Rename $label?'), const Text( 'This will change how the account is displayed in the list.'), TextFormField( @@ -65,6 +67,7 @@ class _RenameAccountDialogState extends ConsumerState { enabled: issuerRemaining > 0, maxLength: issuerRemaining > 0 ? issuerRemaining : null, decoration: const InputDecoration( + border: OutlineInputBorder(), labelText: 'Issuer (optional)', helperText: '', // Prevents dialog resizing when enabled = false ), @@ -78,6 +81,7 @@ class _RenameAccountDialogState extends ConsumerState { initialValue: _account, maxLength: nameRemaining, decoration: InputDecoration( + border: const OutlineInputBorder(), labelText: 'Account name', helperText: '', // Prevents dialog resizing when enabled = false errorText: isValid ? null : 'Your account must have a name', @@ -88,16 +92,15 @@ class _RenameAccountDialogState extends ConsumerState { }); }, ), - ], + ] + .map((e) => Padding( + child: e, + padding: const EdgeInsets.symmetric(vertical: 8.0), + )) + .toList(), ), actions: [ - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ElevatedButton( + TextButton( onPressed: isValid ? () async { final renamed = await ref @@ -113,7 +116,7 @@ class _RenameAccountDialogState extends ConsumerState { ); } : null, - child: const Text('Rename account'), + child: const Text('Save'), ), ], ); diff --git a/lib/settings_page.dart b/lib/settings_page.dart index 58ccf727..911e7f18 100755 --- a/lib/settings_page.dart +++ b/lib/settings_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; -import 'package:yubico_authenticator/app/state.dart'; +import 'app/state.dart'; +import 'app/views/responsive_dialog.dart'; import 'core/state.dart'; final _log = Logger('settings'); @@ -12,48 +13,41 @@ class SettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Theme'), - value: ref.watch(themeModeProvider), - items: [ThemeMode.system, ThemeMode.dark, ThemeMode.light] - .map((e) => DropdownMenuItem( - value: e, - child: Text( - e.name[0].toUpperCase() + e.name.substring(1)), - )) - .toList(), - onChanged: (mode) { - ref.read(themeModeProvider.notifier).setThemeMode(mode!); - }, - ), - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Logging'), - value: ref.watch(logLevelProvider), - items: [Level.INFO, Level.CONFIG, Level.FINE] - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.name.toUpperCase()), - )) - .toList(), - onChanged: (level) { - ref.read(logLevelProvider.notifier).setLogLevel(level!); - _log.config('Log level set to $level'); - }, - ), - ], + return ResponsiveDialog( + title: const Text('Settings'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DropdownButtonFormField( + decoration: const InputDecoration(labelText: 'Theme'), + value: ref.watch(themeModeProvider), + items: [ThemeMode.system, ThemeMode.dark, ThemeMode.light] + .map((e) => DropdownMenuItem( + value: e, + child: + Text(e.name[0].toUpperCase() + e.name.substring(1)), + )) + .toList(), + onChanged: (mode) { + ref.read(themeModeProvider.notifier).setThemeMode(mode!); + }, ), - ), + DropdownButtonFormField( + decoration: const InputDecoration(labelText: 'Logging'), + value: ref.watch(logLevelProvider), + items: [Level.INFO, Level.CONFIG, Level.FINE] + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name.toUpperCase()), + )) + .toList(), + onChanged: (level) { + ref.read(logLevelProvider.notifier).setLogLevel(level!); + _log.config('Log level set to $level'); + }, + ), + ], ), ); } From c945fb401fbe0728e1e64462f7984a06047b124f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 15 Mar 2022 18:10:28 +0100 Subject: [PATCH 2/6] Remove unused parameter. --- lib/oath/views/password_dialog.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/oath/views/password_dialog.dart b/lib/oath/views/password_dialog.dart index c7d83fd1..79378866 100755 --- a/lib/oath/views/password_dialog.dart +++ b/lib/oath/views/password_dialog.dart @@ -10,8 +10,7 @@ import '../state.dart'; class _ManagePasswordForm extends ConsumerStatefulWidget { final DevicePath path; final OathState state; - final Function(bool)? onValid; - const _ManagePasswordForm(this.path, this.state, {this.onValid, Key? key}) + const _ManagePasswordForm(this.path, this.state, {Key? key}) : super(key: key); @override From 21d5e76dccd3abf9c21281e0a06b558b88646cb7 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Mar 2022 09:13:17 +0100 Subject: [PATCH 3/6] Use ResponsiveDialog for 'Delete account'. --- lib/oath/views/delete_account_dialog.dart | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/oath/views/delete_account_dialog.dart b/lib/oath/views/delete_account_dialog.dart index 2581063c..dd2b4457 100755 --- a/lib/oath/views/delete_account_dialog.dart +++ b/lib/oath/views/delete_account_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/views/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; import '../../app/models.dart'; @@ -23,28 +24,27 @@ class DeleteAccountDialog extends ConsumerWidget { ? '${credential.issuer} (${credential.name})' : credential.name; - return AlertDialog( - title: Text('Delete $label?'), - content: Column( - mainAxisSize: MainAxisSize.min, + return ResponsiveDialog( + title: const Text('Delete account'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Warning! This action will delete the account from your YubiKey.'), - const Text(''), Text( 'You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.', style: Theme.of(context).textTheme.bodyText1, ), - ], + Text('Account: $label'), + ] + .map((e) => Padding( + child: e, + padding: const EdgeInsets.symmetric(vertical: 8.0), + )) + .toList(), ), actions: [ - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: const Text('Cancel'), - ), - ElevatedButton( + TextButton( onPressed: () async { await ref .read(credentialListProvider(device.path).notifier) @@ -57,7 +57,7 @@ class DeleteAccountDialog extends ConsumerWidget { ), ); }, - child: const Text('Delete account'), + child: const Text('Delete'), ), ], ); From a58e8d8bdf8af9124392027866f9add6e81208a8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Mar 2022 09:13:46 +0100 Subject: [PATCH 4/6] Add cancel button, use LayoutBuilder. * Adds a cancel button to ResponsiveDialog when in dialog mode. * Use LayoutBuilder instead of MediaQuery. --- lib/app/views/main_page.dart | 39 ++++++++-------- lib/app/views/responsive_dialog.dart | 68 ++++++++++++++++------------ 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 18239566..b6df31f6 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -36,26 +36,27 @@ class MainPage extends ConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { - final query = MediaQuery.of(context); - if (query.size.width < 540) { - // Single column layout - return _buildScaffold(context, ref, true); - } else { - // Two-column layout - return Row( - children: [ - const SizedBox( - width: 240, - child: MainPageDrawer(shouldPop: false), - ), - Expanded( - child: _buildScaffold(context, ref, false), - ), - ], + Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 540) { + // Single column layout + return _buildScaffold(context, ref, true); + } else { + // Two-column layout + return Row( + children: [ + const SizedBox( + width: 240, + child: MainPageDrawer(shouldPop: false), + ), + Expanded( + child: _buildScaffold(context, ref, false), + ), + ], + ); + } + }, ); - } - } Scaffold _buildScaffold(BuildContext context, WidgetRef ref, bool hasDrawer) { final deviceNode = ref.watch(currentDeviceProvider); diff --git a/lib/app/views/responsive_dialog.dart b/lib/app/views/responsive_dialog.dart index 7caa215b..9ea80332 100755 --- a/lib/app/views/responsive_dialog.dart +++ b/lib/app/views/responsive_dialog.dart @@ -3,41 +3,49 @@ import 'package:flutter/material.dart'; class ResponsiveDialog extends StatelessWidget { final Widget? title; final Widget child; - final List? actions; + final List actions; const ResponsiveDialog( - {Key? key, required this.child, this.title, this.actions}) + {Key? key, required this.child, this.title, this.actions = const []}) : super(key: key); @override - Widget build(BuildContext context) { - final query = MediaQuery.of(context); - if (query.size.width < 540) { - // Fullscreen - return Dialog( - insetPadding: const EdgeInsets.all(0), - child: Scaffold( - appBar: AppBar( - title: title, - actions: actions, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(18.0), + Widget build(BuildContext context) => + LayoutBuilder(builder: ((context, constraints) { + if (constraints.maxWidth < 540) { + // Fullscreen + return Dialog( + insetPadding: const EdgeInsets.all(0), + child: Scaffold( + appBar: AppBar( + title: title, + actions: actions, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(18.0), + child: child, + ), + )); + } else { + // Dialog + return AlertDialog( + insetPadding: EdgeInsets.zero, + title: title, + scrollable: true, + content: SizedBox( + width: 380, child: child, ), - )); - } else { - // Dialog - return AlertDialog( - insetPadding: EdgeInsets.zero, - title: title, - scrollable: true, - content: SizedBox( - width: 380, - child: child, - ), - actions: actions, - ); - } - } + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ...actions + ], + ); + } + })); } From 3c712fef3a22316ee8b13fea5e762d0f660e5b8d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Mar 2022 09:18:47 +0100 Subject: [PATCH 5/6] Use ResponsiveDialog for OATH factory reset. --- lib/oath/views/reset_dialog.dart | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/oath/views/reset_dialog.dart b/lib/oath/views/reset_dialog.dart index c8cfdf6d..5d4429cd 100755 --- a/lib/oath/views/reset_dialog.dart +++ b/lib/oath/views/reset_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/views/responsive_dialog.dart'; import '../state.dart'; import '../../app/models.dart'; import '../../app/state.dart'; @@ -16,28 +17,25 @@ class ResetDialog extends ConsumerWidget { Navigator.of(context).pop(); }); - return AlertDialog( - title: const Text('Reset to defaults?'), - content: Column( - mainAxisSize: MainAxisSize.min, + return ResponsiveDialog( + title: const Text('Factory reset'), + child: Column( children: [ const Text( 'Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.'), - const Text(''), Text( 'Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.', style: Theme.of(context).textTheme.bodyText1, ), - ], + ] + .map((e) => Padding( + child: e, + padding: const EdgeInsets.symmetric(vertical: 8.0), + )) + .toList(), ), actions: [ - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ElevatedButton( + TextButton( onPressed: () async { await ref.read(oathStateProvider(device.path).notifier).reset(); Navigator.of(context).pop(); @@ -48,7 +46,7 @@ class ResetDialog extends ConsumerWidget { ), ); }, - child: const Text('Reset YubiKey'), + child: const Text('Reset'), ), ], ); From 0221062feaec1a28782de8084f9c29ff72e3dab9 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Mar 2022 09:30:03 +0100 Subject: [PATCH 6/6] Handle cancelled "delete dialog" return value. --- lib/oath/views/account_mixin.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/oath/views/account_mixin.dart b/lib/oath/views/account_mixin.dart index 8be27385..1fa807a8 100755 --- a/lib/oath/views/account_mixin.dart +++ b/lib/oath/views/account_mixin.dart @@ -152,9 +152,10 @@ mixin AccountMixin { Future deleteCredential(BuildContext context, WidgetRef ref) async { final node = ref.read(currentDeviceProvider)!; return await showDialog( - context: context, - builder: (context) => DeleteAccountDialog(node, credential), - ); + context: context, + builder: (context) => DeleteAccountDialog(node, credential), + ) ?? + false; } @protected