diff --git a/lib/app/models.dart b/lib/app/models.dart index 2e235a1a..115eb14e 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -38,7 +38,7 @@ class DeviceNode with _$DeviceNode { class MenuAction with _$MenuAction { factory MenuAction( {required String text, - required Icon icon, + required Widget icon, void Function(BuildContext context)? action}) = _MenuAction; } diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index f3245477..538415f2 100755 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -656,7 +656,7 @@ class _$MenuActionTearOff { _MenuAction call( {required String text, - required Icon icon, + required Widget icon, void Function(BuildContext)? action}) { return _MenuAction( text: text, @@ -672,7 +672,7 @@ const $MenuAction = _$MenuActionTearOff(); /// @nodoc mixin _$MenuAction { String get text => throw _privateConstructorUsedError; - Icon get icon => throw _privateConstructorUsedError; + Widget get icon => throw _privateConstructorUsedError; void Function(BuildContext)? get action => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -685,7 +685,7 @@ abstract class $MenuActionCopyWith<$Res> { factory $MenuActionCopyWith( MenuAction value, $Res Function(MenuAction) then) = _$MenuActionCopyWithImpl<$Res>; - $Res call({String text, Icon icon, void Function(BuildContext)? action}); + $Res call({String text, Widget icon, void Function(BuildContext)? action}); } /// @nodoc @@ -710,7 +710,7 @@ class _$MenuActionCopyWithImpl<$Res> implements $MenuActionCopyWith<$Res> { icon: icon == freezed ? _value.icon : icon // ignore: cast_nullable_to_non_nullable - as Icon, + as Widget, action: action == freezed ? _value.action : action // ignore: cast_nullable_to_non_nullable @@ -725,7 +725,7 @@ abstract class _$MenuActionCopyWith<$Res> implements $MenuActionCopyWith<$Res> { _MenuAction value, $Res Function(_MenuAction) then) = __$MenuActionCopyWithImpl<$Res>; @override - $Res call({String text, Icon icon, void Function(BuildContext)? action}); + $Res call({String text, Widget icon, void Function(BuildContext)? action}); } /// @nodoc @@ -752,7 +752,7 @@ class __$MenuActionCopyWithImpl<$Res> extends _$MenuActionCopyWithImpl<$Res> icon: icon == freezed ? _value.icon : icon // ignore: cast_nullable_to_non_nullable - as Icon, + as Widget, action: action == freezed ? _value.action : action // ignore: cast_nullable_to_non_nullable @@ -769,7 +769,7 @@ class _$_MenuAction implements _MenuAction { @override final String text; @override - final Icon icon; + final Widget icon; @override final void Function(BuildContext)? action; @@ -804,13 +804,13 @@ class _$_MenuAction implements _MenuAction { abstract class _MenuAction implements MenuAction { factory _MenuAction( {required String text, - required Icon icon, + required Widget icon, void Function(BuildContext)? action}) = _$_MenuAction; @override String get text; @override - Icon get icon; + Widget get icon; @override void Function(BuildContext)? get action; @override diff --git a/lib/oath/state.dart b/lib/oath/state.dart index 917cd63f..8a68adb8 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -50,6 +50,55 @@ abstract class OathCredentialListNotifier Future deleteAccount(OathCredential credential); } +final credentialsProvider = Provider.autoDispose?>((ref) { + final node = ref.watch(currentDeviceProvider); + if (node != null) { + return ref.watch(credentialListProvider(node.path) + .select((pairs) => pairs?.map((e) => e.credential).toList())); + } + return null; +}); + +final codeProvider = + Provider.autoDispose.family((ref, credential) { + final node = ref.watch(currentDeviceProvider); + if (node != null) { + return ref + .watch(credentialListProvider(node.path) + .select((pairs) => pairs?.firstWhere( + (pair) => pair.credential == credential, + orElse: () => OathPair(credential, null), + ))) + ?.code; + } + return null; +}); + +final expiredProvider = + StateNotifierProvider.autoDispose.family<_ExpireNotifier, bool, int>( + (ref, expiry) => + _ExpireNotifier(DateTime.now().millisecondsSinceEpoch, expiry * 1000), +); + +class _ExpireNotifier extends StateNotifier { + Timer? _timer; + _ExpireNotifier(int now, int expiry) : super(expiry <= now) { + if (expiry > now) { + _timer = Timer(Duration(milliseconds: expiry - now), () { + if (mounted) { + state = true; + } + }); + } + } + + @override + dispose() { + _timer?.cancel(); + super.dispose(); + } +} + final favoritesProvider = StateNotifierProvider>( (ref) => FavoritesNotifier(ref.watch(prefProvider))); diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart new file mode 100755 index 00000000..61469bf3 --- /dev/null +++ b/lib/oath/views/account_dialog.dart @@ -0,0 +1,139 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../widgets/circle_timer.dart'; +import '../models.dart'; +import 'account_mixin.dart'; + +class AccountDialog extends ConsumerWidget with AccountMixin { + @override + final OathCredential credential; + const AccountDialog(this.credential, {Key? key}) : super(key: key); + + @override + Future renameCredential( + BuildContext context, WidgetRef ref) async { + final renamed = await super.renameCredential(context, ref); + if (renamed != null) { + // Replace this dialog with a new one, for the renamed credential. + Navigator.of(context).pop(); + await showDialog( + context: context, + builder: (context) { + return AccountDialog(renamed); + }, + ); + } + return renamed; + } + + @override + Future deleteCredential(BuildContext context, WidgetRef ref) async { + final deleted = await super.deleteCredential(context, ref); + if (deleted) { + Navigator.of(context).pop(); + } + return deleted; + } + + List _buildActions(BuildContext context, WidgetRef ref) { + return buildActions(context, ref).map((e) { + final action = e.action; + return IconButton( + icon: e.icon, + tooltip: e.text, + onPressed: action != null + ? () { + action(context); + } + : null, + ); + }).toList(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final code = getCode(ref); + + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0.0, + insetPadding: const EdgeInsets.all(0), + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + actions: _buildActions(context, ref), + ), + body: LayoutBuilder(builder: (context, constraints) { + return ListView( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + alignment: Alignment.center, + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + padding: const EdgeInsets.all(20.0), + child: GestureDetector( + onTap: () {}, // Blocks parent detector GestureDetector + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0)), + elevation: 16.0, + child: SizedBox( + width: 320, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 12.0), + child: Column( + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + formatCode(ref), + softWrap: false, + style: isExpired(ref) + ? Theme.of(context) + .textTheme + .headline2 + ?.copyWith(color: Colors.grey) + : Theme.of(context).textTheme.headline2, + ), + ), + Text(label), + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox.square( + dimension: 16, + child: code != null + ? CircleTimer( + code.validFrom * 1000, + code.validTo * 1000, + ) + : null, + ), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ); + }), + ), + ), + ); + } +} diff --git a/lib/oath/views/account_list.dart b/lib/oath/views/account_list.dart index 8047dfc2..d68db8bc 100755 --- a/lib/oath/views/account_list.dart +++ b/lib/oath/views/account_list.dart @@ -72,28 +72,28 @@ class _AccountListState extends State { ); } - final favCreds = widget.credentials + final pinnedCreds = widget.credentials .where((entry) => widget.favorites.contains(entry.credential.id)); final creds = widget.credentials .where((entry) => !widget.favorites.contains(entry.credential.id)); - _credentials = favCreds.followedBy(creds).map((e) => e.credential).toList(); + _credentials = + pinnedCreds.followedBy(creds).map((e) => e.credential).toList(); _updateFocusNodes(); return ListView( children: [ - if (favCreds.isNotEmpty) + if (pinnedCreds.isNotEmpty) ListTile( title: Text( - 'FAVORITES', + 'PINNED', style: Theme.of(context).textTheme.bodyText2, ), ), - ...favCreds.map( + ...pinnedCreds.map( (entry) => AccountView( widget.deviceData, entry.credential, - entry.code, focusNode: _focusNodes[entry.credential], ), ), @@ -108,7 +108,6 @@ class _AccountListState extends State { (entry) => AccountView( widget.deviceData, entry.credential, - entry.code, focusNode: _focusNodes[entry.credential], ), ), diff --git a/lib/oath/views/account_mixin.dart b/lib/oath/views/account_mixin.dart new file mode 100755 index 00000000..8be27385 --- /dev/null +++ b/lib/oath/views/account_mixin.dart @@ -0,0 +1,234 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../models.dart'; +import '../state.dart'; +import 'delete_account_dialog.dart'; +import 'rename_account_dialog.dart'; + +class _StrikethroughClipper extends CustomClipper { + @override + Path getClip(Size size) { + Path path = Path() + ..moveTo(0, 2) + ..lineTo(0, size.height) + ..lineTo(size.width - 2, size.height) + ..lineTo(0, 2) + ..moveTo(2, 0) + ..lineTo(size.width, size.height - 2) + ..lineTo(size.width, 0) + ..lineTo(2, 0) + ..close(); + return path; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return false; + } +} + +class _StrikethroughPainter extends CustomPainter { + final Color color; + _StrikethroughPainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint(); + paint.color = color; + paint.strokeWidth = 1.3; + canvas.drawLine(Offset(size.width * 0.15, size.height * 0.15), + Offset(size.width * 0.8, size.height * 0.8), paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +mixin AccountMixin { + OathCredential get credential; + + @protected + String get label => credential.issuer != null + ? '${credential.issuer} (${credential.name})' + : credential.name; + + @protected + OathCode? getCode(WidgetRef ref) => ref.watch(codeProvider(credential)); + + @protected + String formatCode(WidgetRef ref) { + final value = getCode(ref)?.value; + if (value == null) { + return '••• •••'; + } else if (value.length < 6) { + return value; + } else { + var i = value.length ~/ 2; + return value.substring(0, i) + ' ' + value.substring(i); + } + } + + @protected + bool isExpired(WidgetRef ref) { + final code = getCode(ref); + return code == null || + (credential.oathType == OathType.totp && + ref.watch(expiredProvider(code.validTo))); + } + + @protected + bool isPinned(WidgetRef ref) => + ref.watch(favoritesProvider).contains(credential.id); + + @protected + Future calculateCode(BuildContext context, WidgetRef ref) async { + Function? close; + if (credential.touchRequired) { + close = ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text('Touch your YubiKey'), + duration: Duration(seconds: 30), + ), + ) + .close; + } else if (credential.oathType == OathType.hotp) { + final showPrompt = Timer(const Duration(milliseconds: 500), () { + close = ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text('Touch your YubiKey'), + duration: Duration(seconds: 30), + ), + ) + .close; + }); + close = showPrompt.cancel; + } + try { + final node = ref.read(currentDeviceProvider)!; + return await ref + .read(credentialListProvider(node.path).notifier) + .calculate(credential); + } finally { + // Hide the touch prompt when done + close?.call(); + } + } + + @protected + void copyToClipboard(BuildContext context, WidgetRef ref) { + final code = getCode(ref); + if (code != null) { + Clipboard.setData(ClipboardData(text: code.value)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Code copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + } + } + + @protected + Future renameCredential( + BuildContext context, WidgetRef ref) async { + final node = ref.read(currentDeviceProvider)!; + return await showDialog( + context: context, + builder: (context) => RenameAccountDialog(node, credential), + ); + } + + @protected + Future deleteCredential(BuildContext context, WidgetRef ref) async { + final node = ref.read(currentDeviceProvider)!; + return await showDialog( + context: context, + builder: (context) => DeleteAccountDialog(node, credential), + ); + } + + @protected + List buildActions(BuildContext context, WidgetRef ref) { + final deviceData = ref.watch(currentDeviceDataProvider); + if (deviceData == null) { + return []; + } + final code = getCode(ref); + final expired = isExpired(ref); + final manual = + credential.touchRequired || credential.oathType == OathType.hotp; + final ready = expired || credential.oathType == OathType.hotp; + final pinned = isPinned(ref); + + return [ + if (manual) + MenuAction( + text: 'Calculate', + icon: const Icon(Icons.refresh), + action: ready + ? (context) { + calculateCode(context, ref); + } + : null, + ), + MenuAction( + text: 'Copy to clipboard', + icon: const Icon(Icons.copy), + action: code == null || expired + ? null + : (context) { + Clipboard.setData(ClipboardData(text: code.value)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Code copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + MenuAction( + text: pinned ? 'Unpin account' : 'Pin account', + //TODO: Replace this with a custom icon. + //Icon(pinned ? Icons.push_pin_remove : Icons.push_pin), + icon: pinned + ? CustomPaint( + painter: _StrikethroughPainter( + Theme.of(context).iconTheme.color ?? Colors.black), + child: ClipPath( + clipper: _StrikethroughClipper(), + child: const Icon(Icons.push_pin)), + ) + : const Icon(Icons.push_pin), + action: (context) { + ref.read(favoritesProvider.notifier).toggleFavorite(credential.id); + }, + ), + if (deviceData.info.version.major >= 5 && + deviceData.info.version.minor >= 3) + MenuAction( + icon: const Icon(Icons.edit), + text: 'Rename account', + action: (context) async { + await renameCredential(context, ref); + }, + ), + MenuAction( + text: 'Delete account', + icon: const Icon(Icons.delete), + action: (context) async { + await deleteCredential(context, ref); + }, + ), + ]; + } +} diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 15513c25..421fd0a0 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -1,175 +1,23 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../widgets/circle_timer.dart'; import '../../app/models.dart'; import '../models.dart'; -import '../state.dart'; -import 'delete_account_dialog.dart'; -import 'rename_account_dialog.dart'; +import 'account_dialog.dart'; +import 'account_mixin.dart'; -final _expireProvider = - StateNotifierProvider.autoDispose.family<_ExpireNotifier, bool, int>( - (ref, expiry) => - _ExpireNotifier(DateTime.now().millisecondsSinceEpoch, expiry * 1000), -); - -class _ExpireNotifier extends StateNotifier { - Timer? _timer; - _ExpireNotifier(int now, int expiry) : super(expiry <= now) { - if (expiry > now) { - _timer = Timer(Duration(milliseconds: expiry - now), () { - if (mounted) { - state = true; - } - }); - } - } - - @override - dispose() { - _timer?.cancel(); - super.dispose(); - } -} - -// TODO: Replace this with something cleaner -final _busyCalculatingProvider = StateProvider((ref) => false); - -class AccountView extends ConsumerWidget { +class AccountView extends ConsumerWidget with AccountMixin { final YubiKeyData deviceData; + @override final OathCredential credential; - final OathCode? code; final FocusNode? focusNode; - const AccountView(this.deviceData, this.credential, this.code, - {Key? key, this.focusNode}) + AccountView(this.deviceData, this.credential, {Key? key, this.focusNode}) : super(key: key); - String formatCode() { - var value = code?.value; - if (value == null) { - return '••• •••'; - } else if (value.length < 6) { - return value; - } else { - var i = value.length ~/ 2; - return value.substring(0, i) + ' ' + value.substring(i); - } - } - - List _buildPopupMenu( - BuildContext context, WidgetRef ref, bool trigger) => - [ - PopupMenuItem( - child: const ListTile( - leading: Icon(Icons.copy), - title: Text('Copy to clipboard'), - ), - onTap: () { - _copyToClipboard(context, ref, trigger); - }, - ), - PopupMenuItem( - child: const ListTile( - leading: Icon(Icons.star), - title: Text('Toggle favorite'), - ), - onTap: () { - ref.read(favoritesProvider.notifier).toggleFavorite(credential.id); - }, - ), - if (deviceData.info.version.major >= 5 && - deviceData.info.version.minor >= 3) - PopupMenuItem( - child: const ListTile( - leading: Icon(Icons.edit), - title: Text('Rename account'), - ), - onTap: () { - // This ensures the onTap handler finishes before the dialog is shown, otherwise the dialog is immediately closed instead of the popup. - Future.delayed(Duration.zero, () { - showDialog( - context: context, - builder: (context) => - RenameAccountDialog(deviceData.node, credential), - ); - }); - }, - ), - const PopupMenuDivider(), - PopupMenuItem( - child: const ListTile( - leading: Icon(Icons.delete_forever), - title: Text('Delete account'), - ), - onTap: () { - // This ensures the onTap handler finishes before the dialog is shown, otherwise the dialog is immediately closed instead of the popup. - Future.delayed(Duration.zero, () { - showDialog( - context: context, - builder: (context) => - DeleteAccountDialog(deviceData.node, credential), - ); - }); - }, - ), - ]; - - _copyToClipboard(BuildContext context, WidgetRef ref, bool trigger) async { - final busy = ref.read(_busyCalculatingProvider.notifier); - if (busy.state) return; - - final scaffoldMessenger = ScaffoldMessenger.of(context); - try { - busy.state = true; - String value; - if (trigger) { - final updated = await _calculate(context, ref); - value = updated.value; - } else { - value = code!.value; - } - await Clipboard.setData(ClipboardData(text: value)); - - await scaffoldMessenger - .showSnackBar( - const SnackBar( - content: Text('Code copied to clipboard'), - duration: Duration(seconds: 2), - ), - ) - .closed; - } finally { - busy.state = false; - } - } - - Future _calculate(BuildContext context, WidgetRef ref) async { - Function? close; - if (credential.touchRequired) { - close = ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text('Touch your YubiKey'), - duration: Duration(seconds: 30), - ), - ) - .close; - } - try { - return await ref - .read(credentialListProvider(deviceData.node.path).notifier) - .calculate(credential); - } finally { - // Hide the touch prompt when done - close?.call(); - } - } - - Color _iconColor(String label, int shade) { + Color _iconColor(int shade) { final colors = [ Colors.red[shade], Colors.pink[shade], @@ -194,86 +42,110 @@ class AccountView extends ConsumerWidget { return colors[label.hashCode % colors.length]!; } + List _buildPopupMenu(BuildContext context, WidgetRef ref) { + return buildActions(context, ref).map((e) { + final action = e.action; + return PopupMenuItem( + child: ListTile( + leading: e.icon, + title: Text(e.text), + dense: true, + contentPadding: EdgeInsets.zero, + ), + enabled: action != null, + onTap: () { + // As soon as onTap returns, the Navigator is popped, + // closing the topmost item. Since we sometimes open new dialogs in + // the action, make sure that happens *after* the pop. + Timer(Duration.zero, () { + action?.call(context); + }); + }, + ); + }).toList(); + } + @override Widget build(BuildContext context, WidgetRef ref) { - final code = this.code; - final label = credential.issuer != null - ? '${credential.issuer} (${credential.name})' - : credential.name; - final expireAt = credential.oathType == OathType.hotp - ? (code?.validFrom ?? 0) + - 30 // HOTP codes valid for 30s from generation - : code?.validTo ?? 0; - final expired = ref.watch(_expireProvider(expireAt)); - final trigger = code == null || - expired && - (credential.touchRequired || credential.oathType == OathType.hotp); + final code = getCode(ref); + final expired = isExpired(ref); + final calculateReady = code == null || + credential.oathType == OathType.hotp || + (credential.touchRequired && expired); final darkMode = Theme.of(context).brightness == Brightness.dark; - return ListTile( - focusNode: focusNode, - onTap: () { - final focus = focusNode; - if (focus != null && focus.hasFocus == false) { - focus.requestFocus(); - } else { - _copyToClipboard(context, ref, trigger); - } - }, - leading: CircleAvatar( - foregroundColor: darkMode ? Colors.black : Colors.white, - backgroundColor: _iconColor(label, darkMode ? 300 : 400), - child: Text( - (credential.issuer ?? credential.name).characters.first.toUpperCase(), - style: const TextStyle(fontSize: 18), - ), - ), - title: Text( - formatCode(), - style: expired - ? Theme.of(context) - .textTheme - .headline5 - ?.copyWith(color: Colors.grey) - : Theme.of(context).textTheme.headline5, - ), - subtitle: Text( - label, - style: Theme.of(context).textTheme.caption, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - Align( - alignment: AlignmentDirectional.topCenter, - child: trigger - ? const Icon( - Icons.touch_app, - size: 18, - ) - : SizedBox.square( - dimension: 16, - child: CircleTimer( - code.validFrom * 1000, - code.validTo * 1000, - ), - ), - ), - const Spacer(), - PopupMenuButton( - child: Icon(Icons.adaptive.more), - itemBuilder: (context) => - _buildPopupMenu(context, ref, trigger), - ), - ], + return GestureDetector( + onSecondaryTapDown: (details) { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + 0, ), - ], + items: _buildPopupMenu(context, ref), + ); + }, + child: ListTile( + focusNode: focusNode, + onTap: () { + showDialog( + context: context, + builder: (context) { + return AccountDialog(credential); + }, + ); + }, + onLongPress: () async { + if (calculateReady) { + await calculateCode( + context, + ref, + ); + } + copyToClipboard(context, ref); + }, + leading: CircleAvatar( + foregroundColor: darkMode ? Colors.black : Colors.white, + backgroundColor: _iconColor(darkMode ? 300 : 400), + child: Text( + (credential.issuer ?? credential.name) + .characters + .first + .toUpperCase(), + style: const TextStyle(fontSize: 18), + ), + ), + title: Text( + formatCode(ref), + style: expired + ? Theme.of(context) + .textTheme + .headline5 + ?.copyWith(color: Colors.grey) + : Theme.of(context).textTheme.headline5, + ), + subtitle: Text( + label, + style: Theme.of(context).textTheme.caption, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ), + trailing: calculateReady + ? Icon( + credential.touchRequired ? Icons.touch_app : Icons.refresh, + size: 18, + ) + : SizedBox.square( + dimension: 16, + child: CircleTimer( + code.validFrom * 1000, + code.validTo * 1000, + ), + ), ), ); } diff --git a/lib/oath/views/delete_account_dialog.dart b/lib/oath/views/delete_account_dialog.dart index 2b8dd349..2581063c 100755 --- a/lib/oath/views/delete_account_dialog.dart +++ b/lib/oath/views/delete_account_dialog.dart @@ -16,7 +16,7 @@ class DeleteAccountDialog extends ConsumerWidget { 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(); + Navigator.of(context).pop(false); }); final label = credential.issuer != null @@ -40,7 +40,7 @@ class DeleteAccountDialog extends ConsumerWidget { actions: [ OutlinedButton( onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(false); }, child: const Text('Cancel'), ), @@ -49,7 +49,7 @@ class DeleteAccountDialog extends ConsumerWidget { await ref .read(credentialListProvider(device.path).notifier) .deleteAccount(credential); - Navigator.of(context).pop(); + Navigator.of(context).pop(true); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Account deleted'), diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index c8beedf6..2f074bff 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -100,11 +100,11 @@ class _RenameAccountDialogState extends ConsumerState { ElevatedButton( onPressed: isValid ? () async { - await ref + final renamed = await ref .read(credentialListProvider(widget.device.path).notifier) .renameAccount(credential, _issuer.isNotEmpty ? _issuer : null, _account); - Navigator.of(context).pop(); + Navigator.of(context).pop(renamed); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Account renamed'), diff --git a/lib/theme.dart b/lib/theme.dart index 2a48d2f9..73d0a579 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -27,6 +27,9 @@ class AppTheme { bodyText2: TextStyle( color: Colors.grey.shade800, ), + headline2: TextStyle( + color: Colors.grey.shade800, + ), ), ); @@ -45,6 +48,9 @@ class AppTheme { bodyText2: TextStyle( color: Colors.grey.shade500, ), + headline2: TextStyle( + color: Colors.grey.shade100, + ), ), ); }