mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 05:52:40 +03:00
Merge PR #1688
This commit is contained in:
commit
5a24f57e0b
@ -28,7 +28,7 @@ abstract class AppContextManager {
|
|||||||
|
|
||||||
open fun onPause() {}
|
open fun onPause() {}
|
||||||
|
|
||||||
open fun onError() {}
|
open fun onError(e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContextDisposedException : Exception()
|
class ContextDisposedException : Exception()
|
@ -79,6 +79,7 @@ import kotlinx.coroutines.launch
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import java.io.IOException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
@ -310,43 +311,55 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun processYubiKey(device: YubiKeyDevice) {
|
private suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||||
val deviceInfo = getDeviceInfo(device)
|
val deviceInfo = try {
|
||||||
|
|
||||||
if (deviceInfo == null) {
|
if (device is NfcYubiKeyDevice) {
|
||||||
deviceManager.setDeviceInfo(null)
|
appMethodChannel.nfcStateChanged(NfcState.ONGOING)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device is NfcYubiKeyDevice) {
|
|
||||||
appMethodChannel.nfcStateChanged(NfcState.ONGOING)
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceManager.scpKeyParams = null
|
|
||||||
// If NFC and FIPS check for SCP11b key
|
|
||||||
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
|
||||||
logger.debug("Checking for usable SCP11b key...")
|
|
||||||
deviceManager.scpKeyParams = try {
|
|
||||||
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
|
||||||
val scp = SecurityDomainSession(connection)
|
|
||||||
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
|
||||||
keyRef?.let {
|
|
||||||
val certs = scp.getCertificateBundle(it)
|
|
||||||
if (certs.isNotEmpty()) Scp11KeyParams(
|
|
||||||
keyRef,
|
|
||||||
certs[certs.size - 1].publicKey
|
|
||||||
) else null
|
|
||||||
}?.also {
|
|
||||||
logger.debug("Found SCP11b key: {}", keyRef)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.debug("Exception while getting scp keys: ", e)
|
|
||||||
contextManager?.onError()
|
|
||||||
if (device is NfcYubiKeyDevice) {
|
|
||||||
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deviceInfo = getDeviceInfo(device)
|
||||||
|
|
||||||
|
deviceManager.scpKeyParams = null
|
||||||
|
// If NFC and FIPS check for SCP11b key
|
||||||
|
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
||||||
|
logger.debug("Checking for usable SCP11b key...")
|
||||||
|
deviceManager.scpKeyParams = try {
|
||||||
|
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
||||||
|
val scp = SecurityDomainSession(connection)
|
||||||
|
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
||||||
|
keyRef?.let {
|
||||||
|
val certs = scp.getCertificateBundle(it)
|
||||||
|
if (certs.isNotEmpty()) Scp11KeyParams(
|
||||||
|
keyRef,
|
||||||
|
certs[certs.size - 1].publicKey
|
||||||
|
) else null
|
||||||
|
}?.also {
|
||||||
|
logger.debug("Found SCP11b key: {}", keyRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Exception when reading SCP key information: ", e)
|
||||||
|
// we throw IO exception to unify handling failures as we don't want
|
||||||
|
// th clear device info
|
||||||
|
throw IOException("Failure getting SCP keys")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deviceInfo
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.debug("Exception while getting device info and scp keys: ", e)
|
||||||
|
contextManager?.onError(e)
|
||||||
|
if (device is NfcYubiKeyDevice) {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not clear deviceInfo on IOExceptions,
|
||||||
|
// this allows for retries of failed actions
|
||||||
|
if (e !is IOException) {
|
||||||
|
logger.debug("Resetting device info")
|
||||||
|
deviceManager.setDeviceInfo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC
|
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC
|
||||||
|
@ -42,14 +42,6 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
|
|||||||
return requestHandled
|
return requestHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
fun failPending(e: Exception) {
|
|
||||||
pendingAction?.let { action ->
|
|
||||||
logger.error("Failing pending action with {}", e.message)
|
|
||||||
action.invoke(Result.failure(e))
|
|
||||||
pendingAction = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelPending() {
|
fun cancelPending() {
|
||||||
pendingAction?.let { action ->
|
pendingAction?.let { action ->
|
||||||
action.invoke(Result.failure(CancellationException()))
|
action.invoke(Result.failure(CancellationException()))
|
||||||
@ -80,7 +72,7 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
|
|||||||
block(YubiKitFidoSession(it))
|
block(YubiKitFidoSession(it))
|
||||||
}.also {
|
}.also {
|
||||||
if (updateDeviceInfo) {
|
if (updateDeviceInfo) {
|
||||||
deviceManager.setDeviceInfo(getDeviceInfo(device))
|
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,9 +174,9 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError() {
|
override fun onError(e: Exception) {
|
||||||
super.onError()
|
super.onError(e)
|
||||||
logger.debug("Cancel any pending action because of upstream error")
|
logger.error("Cancelling pending action. Cause: ", e)
|
||||||
connectionHelper.cancelPending()
|
connectionHelper.cancelPending()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,13 +204,12 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateDeviceInfo.getAndSet(false)) {
|
if (updateDeviceInfo.getAndSet(false)) {
|
||||||
deviceManager.setDeviceInfo(getDeviceInfo(device))
|
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// something went wrong, try to get DeviceInfo from any available connection type
|
|
||||||
logger.error("Failure when processing YubiKey: ", e)
|
|
||||||
|
|
||||||
connectionHelper.failPending(e)
|
logger.error("Cancelling pending action. Cause: ", e)
|
||||||
|
connectionHelper.cancelPending()
|
||||||
|
|
||||||
if (e !is IOException) {
|
if (e !is IOException) {
|
||||||
// we don't clear the session on IOExceptions so that the session is ready for
|
// we don't clear the session on IOExceptions so that the session is ready for
|
||||||
@ -240,7 +239,7 @@ class FidoManager(
|
|||||||
currentSession
|
currentSession
|
||||||
)
|
)
|
||||||
|
|
||||||
val sameDevice = currentSession == previousSession
|
val sameDevice = currentSession.sameDevice(previousSession)
|
||||||
|
|
||||||
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
|
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
|
||||||
requestHandled = connectionHelper.invokePending(fidoSession)
|
requestHandled = connectionHelper.invokePending(fidoSession)
|
||||||
|
@ -41,6 +41,19 @@ data class Options(
|
|||||||
infoData.getOptionsBoolean("ep"),
|
infoData.getOptionsBoolean("ep"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun sameDevice(other: Options) : Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
|
||||||
|
if (clientPin != other.clientPin) return false
|
||||||
|
if (credMgmt != other.credMgmt) return false
|
||||||
|
if (credentialMgmtPreview != other.credentialMgmtPreview) return false
|
||||||
|
if (bioEnroll != other.bioEnroll) return false
|
||||||
|
// alwaysUv may differ
|
||||||
|
// ep may differ
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun InfoData.getOptionsBoolean(
|
private fun InfoData.getOptionsBoolean(
|
||||||
key: String
|
key: String
|
||||||
@ -67,6 +80,21 @@ data class SessionInfo(
|
|||||||
infoData.remainingDiscoverableCredentials
|
infoData.remainingDiscoverableCredentials
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// this is a more permissive comparison, which does not take in an account properties,
|
||||||
|
// which might change by using the FIDO authenticator
|
||||||
|
fun sameDevice(other: SessionInfo?): Boolean {
|
||||||
|
if (other == null) return false
|
||||||
|
if (this === other) return true
|
||||||
|
|
||||||
|
if (!options.sameDevice(other.options)) return false
|
||||||
|
if (!aaguid.contentEquals(other.aaguid)) return false
|
||||||
|
// minPinLength may differ
|
||||||
|
// forcePinChange may differ
|
||||||
|
// remainingDiscoverableCredentials may differ
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -110,9 +110,9 @@ class OathManager(
|
|||||||
private val updateDeviceInfo = AtomicBoolean(false)
|
private val updateDeviceInfo = AtomicBoolean(false)
|
||||||
private var deviceInfoTimer: TimerTask? = null
|
private var deviceInfoTimer: TimerTask? = null
|
||||||
|
|
||||||
override fun onError() {
|
override fun onError(e: Exception) {
|
||||||
super.onError()
|
super.onError(e)
|
||||||
logger.debug("Cancel any pending action because of upstream error")
|
logger.error("Cancelling pending action in onError. Cause: ", e)
|
||||||
pendingAction?.let { action ->
|
pendingAction?.let { action ->
|
||||||
action.invoke(Result.failure(CancellationException()))
|
action.invoke(Result.failure(CancellationException()))
|
||||||
pendingAction = null
|
pendingAction = null
|
||||||
@ -343,16 +343,16 @@ class OathManager(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (updateDeviceInfo.getAndSet(false)) {
|
if (updateDeviceInfo.getAndSet(false)) {
|
||||||
deviceManager.setDeviceInfo(getDeviceInfo(device))
|
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
||||||
logger.error("Exception during SmartCard connection/OATH session creation: ", e)
|
logger.error("Exception during SmartCard connection/OATH session creation: ", e)
|
||||||
|
|
||||||
// Remove any pending action
|
// Cancel any pending action
|
||||||
pendingAction?.let { action ->
|
pendingAction?.let { action ->
|
||||||
logger.error("Failing pending action with {}", e.message)
|
logger.error("Cancelling pending action. Cause: ", e)
|
||||||
action.invoke(Result.failure(e))
|
action.invoke(Result.failure(CancellationException()))
|
||||||
pendingAction = null
|
pendingAction = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -782,7 +782,7 @@ class OathManager(
|
|||||||
block(getOathSession(it))
|
block(getOathSession(it))
|
||||||
}.also {
|
}.also {
|
||||||
if (updateDeviceInfo) {
|
if (updateDeviceInfo) {
|
||||||
deviceManager.setDeviceInfo(getDeviceInfo(device))
|
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,17 +47,17 @@ class DeviceInfoHelper {
|
|||||||
private val restrictedNfcBytes =
|
private val restrictedNfcBytes =
|
||||||
byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri
|
byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri
|
||||||
|
|
||||||
suspend fun getDeviceInfo(device: YubiKeyDevice): Info? {
|
suspend fun getDeviceInfo(device: YubiKeyDevice): Info {
|
||||||
SessionVersionOverride.set(null)
|
SessionVersionOverride.set(null)
|
||||||
var deviceInfo = readDeviceInfo(device)
|
var deviceInfo = readDeviceInfo(device)
|
||||||
if (deviceInfo?.version?.major == 0.toByte()) {
|
if (deviceInfo.version.major == 0.toByte()) {
|
||||||
SessionVersionOverride.set(Version(5, 7, 2))
|
SessionVersionOverride.set(Version(5, 7, 2))
|
||||||
deviceInfo = readDeviceInfo(device)
|
deviceInfo = readDeviceInfo(device)
|
||||||
}
|
}
|
||||||
return deviceInfo
|
return deviceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun readDeviceInfo(device: YubiKeyDevice): Info? {
|
private suspend fun readDeviceInfo(device: YubiKeyDevice): Info {
|
||||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||||
|
|
||||||
val deviceInfo = runCatching {
|
val deviceInfo = runCatching {
|
||||||
@ -106,8 +106,8 @@ class DeviceInfoHelper {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// no smart card connectivity
|
// no smart card connectivity
|
||||||
logger.error("Failure getting device info", e)
|
logger.error("Failure getting device info: ", e)
|
||||||
return null
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,9 +365,8 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
|||||||
var decodedException = pe.decode();
|
var decodedException = pe.decode();
|
||||||
if (decodedException is CancellationException) {
|
if (decodedException is CancellationException) {
|
||||||
_log.debug('User cancelled delete credential FIDO operation');
|
_log.debug('User cancelled delete credential FIDO operation');
|
||||||
} else {
|
|
||||||
throw decodedException;
|
|
||||||
}
|
}
|
||||||
|
throw decodedException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,8 @@ class MainPage extends ConsumerWidget {
|
|||||||
final prevSerial =
|
final prevSerial =
|
||||||
prev?.hasValue == true ? prev?.value?.info.serial : null;
|
prev?.hasValue == true ? prev?.value?.info.serial : null;
|
||||||
if ((serial != null && serial == prevSerial) ||
|
if ((serial != null && serial == prevSerial) ||
|
||||||
(next.hasValue && (prev != null && prev.isLoading))) {
|
(next.hasValue && (prev != null && prev.isLoading)) ||
|
||||||
|
next.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2022-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
@ -57,15 +58,19 @@ class DeleteCredentialDialog extends ConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref
|
try {
|
||||||
.read(credentialProvider(devicePath).notifier)
|
await ref
|
||||||
.deleteCredential(credential);
|
.read(credentialProvider(devicePath).notifier)
|
||||||
await ref.read(withContextProvider)(
|
.deleteCredential(credential);
|
||||||
(context) async {
|
await ref.read(withContextProvider)(
|
||||||
Navigator.of(context).pop(true);
|
(context) async {
|
||||||
showMessage(context, l10n.s_passkey_deleted);
|
Navigator.of(context).pop(true);
|
||||||
},
|
showMessage(context, l10n.s_passkey_deleted);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} on CancellationException catch (_) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(l10n.s_delete),
|
child: Text(l10n.s_delete),
|
||||||
),
|
),
|
||||||
|
@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../../widgets/app_input_decoration.dart';
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
@ -71,23 +72,28 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
_submit() async {
|
_submit() async {
|
||||||
_removeFocus();
|
_removeFocus();
|
||||||
|
|
||||||
final result = await ref
|
try {
|
||||||
.read(oathStateProvider(widget.path).notifier)
|
final result = await ref
|
||||||
.setPassword(_currentPasswordController.text, _newPassword);
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
if (result) {
|
.setPassword(_currentPasswordController.text, _newPassword);
|
||||||
if (mounted) {
|
if (result) {
|
||||||
await ref.read(withContextProvider)((context) async {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
await ref.read(withContextProvider)((context) async {
|
||||||
showMessage(context, AppLocalizations.of(context)!.s_password_set);
|
Navigator.of(context).pop();
|
||||||
|
showMessage(context, AppLocalizations.of(context)!.s_password_set);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_currentPasswordController.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: _currentPasswordController.text.length);
|
||||||
|
_currentPasswordFocus.requestFocus();
|
||||||
|
setState(() {
|
||||||
|
_currentIsWrong = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} on CancellationException catch (_) {
|
||||||
_currentPasswordController.selection = TextSelection(
|
// ignored
|
||||||
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
|
|
||||||
_currentPasswordFocus.requestFocus();
|
|
||||||
setState(() {
|
|
||||||
_currentIsWrong = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,49 +67,15 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
|
|||||||
OathCredential credential,
|
OathCredential credential,
|
||||||
List<(String? issuer, String name)> existing) {
|
List<(String? issuer, String name)> existing) {
|
||||||
return RenameAccountDialog(
|
return RenameAccountDialog(
|
||||||
devicePath: devicePath,
|
devicePath: devicePath,
|
||||||
issuer: credential.issuer,
|
issuer: credential.issuer,
|
||||||
name: credential.name,
|
name: credential.name,
|
||||||
oathType: credential.oathType,
|
oathType: credential.oathType,
|
||||||
period: credential.period,
|
period: credential.period,
|
||||||
existing: existing,
|
existing: existing,
|
||||||
rename: (issuer, name) async {
|
rename: (issuer, name) async => await ref
|
||||||
final withContext = ref.read(withContextProvider);
|
.read(credentialListProvider(devicePath).notifier)
|
||||||
try {
|
.renameAccount(credential, issuer, name));
|
||||||
// Rename credentials
|
|
||||||
final renamed = await ref
|
|
||||||
.read(credentialListProvider(devicePath).notifier)
|
|
||||||
.renameAccount(credential, issuer, name);
|
|
||||||
|
|
||||||
// Update favorite
|
|
||||||
ref
|
|
||||||
.read(favoritesProvider.notifier)
|
|
||||||
.renameCredential(credential.id, renamed.id);
|
|
||||||
|
|
||||||
await withContext((context) async => showMessage(
|
|
||||||
context, AppLocalizations.of(context)!.s_account_renamed));
|
|
||||||
return renamed;
|
|
||||||
} on CancellationException catch (_) {
|
|
||||||
// ignored
|
|
||||||
} catch (e) {
|
|
||||||
_log.error('Failed to rename account', e);
|
|
||||||
final String errorMessage;
|
|
||||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
|
||||||
if (e is RpcError) {
|
|
||||||
errorMessage = e.message;
|
|
||||||
} else {
|
|
||||||
errorMessage = e.toString();
|
|
||||||
}
|
|
||||||
await withContext((context) async => showMessage(
|
|
||||||
context,
|
|
||||||
AppLocalizations.of(context)!
|
|
||||||
.l_rename_account_failed(errorMessage),
|
|
||||||
duration: const Duration(seconds: 4),
|
|
||||||
));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,9 +104,39 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
_issuerFocus.unfocus();
|
_issuerFocus.unfocus();
|
||||||
_nameFocus.unfocus();
|
_nameFocus.unfocus();
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
final renamed =
|
final withContext = ref.read(withContextProvider);
|
||||||
await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name);
|
|
||||||
nav.pop(renamed);
|
try {
|
||||||
|
// Rename credentials
|
||||||
|
final renamed =
|
||||||
|
await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name);
|
||||||
|
|
||||||
|
// Update favorite
|
||||||
|
ref
|
||||||
|
.read(favoritesProvider.notifier)
|
||||||
|
.renameCredential(renamed.id, renamed.id);
|
||||||
|
|
||||||
|
await withContext((context) async => showMessage(
|
||||||
|
context, AppLocalizations.of(context)!.s_account_renamed));
|
||||||
|
|
||||||
|
nav.pop(renamed);
|
||||||
|
} on CancellationException catch (_) {
|
||||||
|
// ignored
|
||||||
|
} catch (e) {
|
||||||
|
_log.error('Failed to rename account', e);
|
||||||
|
final String errorMessage;
|
||||||
|
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||||
|
if (e is RpcError) {
|
||||||
|
errorMessage = e.message;
|
||||||
|
} else {
|
||||||
|
errorMessage = e.toString();
|
||||||
|
}
|
||||||
|
await withContext((context) async => showMessage(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.l_rename_account_failed(errorMessage),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
Reference in New Issue
Block a user