This commit is contained in:
Dain Nilsson 2022-03-03 14:24:58 +01:00
commit 79de2dd2e1
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
10 changed files with 553 additions and 254 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -50,6 +50,55 @@ abstract class OathCredentialListNotifier
Future<void> deleteAccount(OathCredential credential);
}
final credentialsProvider = Provider.autoDispose<List<OathCredential>?>((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<OathCode?, OathCredential>((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<bool> {
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<FavoritesNotifier, List<String>>(
(ref) => FavoritesNotifier(ref.watch(prefProvider)));

View File

@ -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<OathCredential?> 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<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
final deleted = await super.deleteCredential(context, ref);
if (deleted) {
Navigator.of(context).pop();
}
return deleted;
}
List<Widget> _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,
),
)
],
),
),
),
),
),
),
),
],
);
}),
),
),
);
}
}

View File

@ -72,28 +72,28 @@ class _AccountListState extends State<AccountList> {
);
}
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<AccountList> {
(entry) => AccountView(
widget.deviceData,
entry.credential,
entry.code,
focusNode: _focusNodes[entry.credential],
),
),

234
lib/oath/views/account_mixin.dart Executable file
View File

@ -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<Path> {
@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<Path> 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<OathCode> 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<OathCredential?> renameCredential(
BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!;
return await showDialog(
context: context,
builder: (context) => RenameAccountDialog(node, credential),
);
}
@protected
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!;
return await showDialog(
context: context,
builder: (context) => DeleteAccountDialog(node, credential),
);
}
@protected
List<MenuAction> 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);
},
),
];
}
}

View File

@ -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<bool> {
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<bool>((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<PopupMenuEntry> _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<OathCode> _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<PopupMenuItem> _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,
),
),
),
);
}

View File

@ -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<DeviceNode?>(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'),

View File

@ -100,11 +100,11 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
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'),

View File

@ -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,
),
),
);
}