use ViewModelData interface

This commit is contained in:
Adam Velebil 2024-03-13 10:36:50 +01:00
parent ff8f2c8ce0
commit 9a19a9c608
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
18 changed files with 112 additions and 153 deletions

View File

@ -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.
@ -36,18 +36,12 @@ interface JsonSerializable {
fun toJson() : String fun toJson() : String
} }
sealed interface SessionState { sealed interface ViewModelData {
data object Empty : SessionState data object Empty : ViewModelData
data object Loading : SessionState data object Loading : ViewModelData
data class Value<T : JsonSerializable>(val data: T) : SessionState data class Value<T : JsonSerializable>(val data: T) : ViewModelData
} }
data class ChannelData<T>(val data: T?, val isLoading: Boolean = false) {
companion object {
fun <T> loading() = ChannelData<T>(null, true)
fun <T> empty() = ChannelData<T>(null)
}
}
/** /**
* Observes a LiveData value, sending each change to Flutter via an EventChannel. * Observes a LiveData value, sending each change to Flutter via an EventChannel.
*/ */
@ -78,55 +72,20 @@ inline fun <reified T> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, mess
} }
/** /**
* Observes a Loadable LiveData value, sending each change to Flutter via an EventChannel. * Observes a ViewModelData LiveData value, sending each change to Flutter via an EventChannel.
*/ */
inline fun <reified T> LiveData<ChannelData<T>>.streamData(lifecycleOwner: LifecycleOwner, messenger: BinaryMessenger, channelName: String): Closeable { @JvmName("streamViewModelData")
inline fun <reified T : ViewModelData> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, messenger: BinaryMessenger, channelName: String): Closeable {
val channel = EventChannel(messenger, channelName) val channel = EventChannel(messenger, channelName)
var sink: EventChannel.EventSink? = null var sink: EventChannel.EventSink? = null
channel.setStreamHandler(object : EventChannel.StreamHandler { val get: (ViewModelData) -> String = {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) { when (it) {
sink = events is ViewModelData.Empty -> NULL
events.success( is ViewModelData.Loading -> LOADING
value?.let { is ViewModelData.Value<*> -> it.data.toJson()
if (it.isLoading) LOADING
else it.data?.let(jsonSerializer::encodeToString) ?: NULL
} ?: NULL
)
} }
override fun onCancel(arguments: Any?) {
sink = null
} }
})
val observer = Observer<ChannelData<T>> {
sink?.success(
if (it.isLoading) LOADING
else it.data?.let(jsonSerializer::encodeToString) ?: NULL
)
}
observe(lifecycleOwner, observer)
return Closeable {
removeObserver(observer)
channel.setStreamHandler(null)
}
}
fun get(state: SessionState) : String = when (state) {
is SessionState.Empty -> NULL
is SessionState.Loading -> LOADING
is SessionState.Value<*> -> state.data.toJson()
}
/**
* Observes a Loadable LiveData value, sending each change to Flutter via an EventChannel.
*/
inline fun <reified T : SessionState> LiveData<T>.streamState(lifecycleOwner: LifecycleOwner, messenger: BinaryMessenger, channelName: String): Closeable {
val channel = EventChannel(messenger, channelName)
var sink: EventChannel.EventSink? = null
channel.setStreamHandler(object : EventChannel.StreamHandler { channel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) { override fun onListen(arguments: Any?, events: EventChannel.EventSink) {

View File

@ -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.
@ -20,7 +20,7 @@ import kotlinx.serialization.json.Json
const val NULL = "null" const val NULL = "null"
const val LOADING = """{ "loading": true }""" const val LOADING = "\"loading\""
val jsonSerializer = Json { val jsonSerializer = Json {
// creates properties for default values // creates properties for default values

View File

@ -320,10 +320,10 @@ class MainActivity : FlutterFragmentActivity() {
flutterStreams = listOf( flutterStreams = listOf(
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"), viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
oathViewModel.sessionState.streamState(this, messenger, "android.oath.sessionState"), oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"),
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"), oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
fidoViewModel.sessionState.streamData(this, messenger, "android.fido.sessionState"), fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
fidoViewModel.credentials.streamData(this, messenger, "android.fido.credentials"), fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"),
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"), fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
) )

View File

@ -136,14 +136,6 @@ class FidoManager(
else -> throw NotImplementedError() else -> throw NotImplementedError()
} }
} }
if (!deviceManager.isUsbKeyConnected()) {
// for NFC connections require extra tap when switching context
if (fidoViewModel.sessionState.value == null) {
fidoViewModel.clearSessionState()
}
}
} }
override fun dispose() { override fun dispose() {
@ -151,7 +143,7 @@ class FidoManager(
deviceManager.removeDeviceListener(this) deviceManager.removeDeviceListener(this)
fidoChannel.setMethodCallHandler(null) fidoChannel.setMethodCallHandler(null)
fidoViewModel.clearSessionState() fidoViewModel.clearSessionState()
fidoViewModel.clearCredentials() fidoViewModel.updateCredentials(emptyList())
coroutineScope.cancel() coroutineScope.cancel()
} }
@ -198,7 +190,7 @@ class FidoManager(
YubiKitFidoSession(connection as SmartCardConnection) YubiKitFidoSession(connection as SmartCardConnection)
} }
val previousSession = fidoViewModel.sessionState.value?.data?.info val previousSession = fidoViewModel.currentSession()?.info
val currentSession = SessionInfo(fidoSession.cachedInfo) val currentSession = SessionInfo(fidoSession.cachedInfo)
logger.debug( logger.debug(
"Previous session: {}, current session: {}", "Previous session: {}, current session: {}",
@ -286,7 +278,7 @@ class FidoManager(
ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED
) { ) {
pinStore.setPin(null) pinStore.setPin(null)
fidoViewModel.clearCredentials() fidoViewModel.updateCredentials(emptyList())
val pinRetriesResult = clientPin.pinRetries val pinRetriesResult = clientPin.pinRetries
JSONObject( JSONObject(
mapOf( mapOf(
@ -356,9 +348,6 @@ class FidoManager(
pinUvAuthToken: ByteArray pinUvAuthToken: ByteArray
): List<FidoCredential> = ): List<FidoCredential> =
try { try {
fidoViewModel.setCredentialsLoadingState()
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, pinUvAuthToken) val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
val rpIds = credMan.enumerateRps() val rpIds = credMan.enumerateRps()
@ -394,7 +383,7 @@ class FidoManager(
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token) val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
val credentialDescriptor = val credentialDescriptor =
fidoViewModel.credentials.value?.data?.firstOrNull { fidoViewModel.credentials.value?.firstOrNull {
it.credentialId == credentialId && it.rpId == rpId it.credentialId == credentialId && it.rpId == rpId
}?.publicKeyCredentialDescriptor }?.publicKeyCredentialDescriptor

View File

@ -202,7 +202,7 @@ class FidoResetHelper(
logger.debug("Calling FIDO reset") logger.debug("Calling FIDO reset")
fidoSession.reset(resetCommandState) fidoSession.reset(resetCommandState)
fidoViewModel.setSessionState(Session(fidoSession.info, true)) fidoViewModel.setSessionState(Session(fidoSession.info, true))
fidoViewModel.clearCredentials() fidoViewModel.updateCredentials(emptyList())
pinStore.setPin(null) pinStore.setPin(null)
} }

View File

@ -19,45 +19,39 @@ package com.yubico.authenticator.fido
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.yubico.authenticator.ChannelData import com.yubico.authenticator.ViewModelData
import com.yubico.authenticator.fido.data.FidoCredential import com.yubico.authenticator.fido.data.FidoCredential
import com.yubico.authenticator.fido.data.Session import com.yubico.authenticator.fido.data.Session
class FidoViewModel : ViewModel() { class FidoViewModel : ViewModel() {
private val _sessionState = MutableLiveData<ChannelData<Session?>>() private val _sessionState = MutableLiveData<ViewModelData>()
val sessionState: LiveData<ChannelData<Session?>> = _sessionState val sessionState: LiveData<ViewModelData> = _sessionState
fun setSessionState(sessionState: Session?) { fun currentSession() : Session? = (_sessionState.value as? ViewModelData.Value<*>)?.data as? Session?
_sessionState.postValue(ChannelData(sessionState))
fun setSessionState(sessionState: Session) {
_sessionState.postValue(ViewModelData.Value(sessionState))
} }
fun clearSessionState() { fun clearSessionState() {
_sessionState.postValue(ChannelData.empty()) _sessionState.postValue(ViewModelData.Empty)
} }
fun setSessionLoadingState() { fun setSessionLoadingState() {
_sessionState.postValue(ChannelData.loading()) _sessionState.postValue(ViewModelData.Loading)
} }
private val _credentials = MutableLiveData<ChannelData<List<FidoCredential>?>>() private val _credentials = MutableLiveData<List<FidoCredential>>()
val credentials: LiveData<ChannelData<List<FidoCredential>?>> = _credentials val credentials: LiveData<List<FidoCredential>> = _credentials
fun setCredentialsLoadingState() {
_credentials.postValue(ChannelData.loading())
}
fun updateCredentials(credentials: List<FidoCredential>) { fun updateCredentials(credentials: List<FidoCredential>) {
_credentials.postValue(ChannelData(credentials)) _credentials.postValue(credentials)
}
fun clearCredentials() {
_credentials.postValue(ChannelData.empty())
} }
fun removeCredential(rpId: String, credentialId: String) { fun removeCredential(rpId: String, credentialId: String) {
_credentials.postValue(ChannelData(_credentials.value?.data?.filter { _credentials.postValue(_credentials.value?.filter {
it.credentialId != credentialId || it.rpId != rpId it.credentialId != credentialId || it.rpId != rpId
})) })
} }
private val _resetState = MutableLiveData(FidoResetState.Remove.value) private val _resetState = MutableLiveData(FidoResetState.Remove.value)

View File

@ -16,6 +16,8 @@
package com.yubico.authenticator.fido.data package com.yubico.authenticator.fido.data
import com.yubico.authenticator.JsonSerializable
import com.yubico.authenticator.jsonSerializer
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
import kotlinx.serialization.* import kotlinx.serialization.*
@ -87,8 +89,12 @@ data class Session(
@SerialName("info") @SerialName("info")
val info: SessionInfo, val info: SessionInfo,
val unlocked: Boolean val unlocked: Boolean
) { ) : JsonSerializable {
constructor(infoData: InfoData, unlocked: Boolean) : this( constructor(infoData: InfoData, unlocked: Boolean) : this(
SessionInfo(infoData), unlocked SessionInfo(infoData), unlocked
) )
override fun toJson(): String {
return jsonSerializer.encodeToString(this)
}
} }

View File

@ -200,13 +200,6 @@ class OathManager(
else -> throw NotImplementedError() else -> throw NotImplementedError()
} }
} }
if (!deviceManager.isUsbKeyConnected()) {
// for NFC connections require extra tap when switching context
if (oathViewModel.sessionState.value is SessionState.Empty) {
oathViewModel.setSessionState(null)
}
}
} }
override fun dispose() { override fun dispose() {
@ -214,7 +207,7 @@ class OathManager(
deviceManager.removeDeviceListener(this) deviceManager.removeDeviceListener(this)
oathViewModel.credentials.removeObserver(credentialObserver) oathViewModel.credentials.removeObserver(credentialObserver)
oathChannel.setMethodCallHandler(null) oathChannel.setMethodCallHandler(null)
oathViewModel.setSessionState(null) oathViewModel.clearSession()
oathViewModel.updateCredentials(mapOf()) oathViewModel.updateCredentials(mapOf())
coroutineScope.cancel() coroutineScope.cancel()
} }
@ -325,7 +318,7 @@ class OathManager(
} }
// Clear any cached OATH state // Clear any cached OATH state
oathViewModel.setSessionState(null) oathViewModel.clearSession()
} }
} }
@ -752,10 +745,10 @@ class OathManager(
override fun onDisconnected() { override fun onDisconnected() {
refreshJob?.cancel() refreshJob?.cancel()
oathViewModel.setSessionState(null) oathViewModel.clearSession()
} }
override fun onTimeout() { override fun onTimeout() {
oathViewModel.setSessionState(null) oathViewModel.clearSession()
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022-2023 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.
@ -19,8 +19,7 @@ package com.yubico.authenticator.oath
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.yubico.authenticator.SessionState import com.yubico.authenticator.ViewModelData
import com.yubico.authenticator.JsonSerializable
import com.yubico.authenticator.oath.data.Code import com.yubico.authenticator.oath.data.Code
import com.yubico.authenticator.oath.data.Credential import com.yubico.authenticator.oath.data.Credential
import com.yubico.authenticator.oath.data.CredentialWithCode import com.yubico.authenticator.oath.data.CredentialWithCode
@ -28,32 +27,31 @@ import com.yubico.authenticator.oath.data.Session
class OathViewModel: ViewModel() { class OathViewModel: ViewModel() {
private val _sessionState = MutableLiveData<SessionState>() private val _sessionState = MutableLiveData<ViewModelData>()
val sessionState: LiveData<SessionState> = _sessionState val sessionState: LiveData<ViewModelData> = _sessionState
fun currentSession() : Session? = (sessionState.value as? SessionState.Value<*>)?.data as Session? fun currentSession() : Session? = (_sessionState.value as? ViewModelData.Value<*>)?.data as? Session?
// Sets session and credentials after performing OATH reset // Sets session and credentials after performing OATH reset
// Note: we cannot use [setSessionState] because resetting OATH changes deviceId // Note: we cannot use [setSessionState] because resetting OATH changes deviceId
fun resetOathSession(sessionState: Session, credentials: Map<Credential, Code?>) { fun resetOathSession(sessionState: Session, credentials: Map<Credential, Code?>) {
_sessionState.postValue(SessionState.Value(sessionState)) _sessionState.postValue(ViewModelData.Value(sessionState))
updateCredentials(credentials) updateCredentials(credentials)
} }
fun setSessionState(sessionState: Session?) { fun setSessionState(sessionState: Session) {
val oldDeviceId = currentSession()?.deviceId val oldDeviceId = currentSession()?.deviceId
_sessionState.postValue(ViewModelData.Value(sessionState))
if (sessionState == null) { if(oldDeviceId != sessionState.deviceId) {
_sessionState.postValue(SessionState.Empty)
} else {
_sessionState.postValue(SessionState.Value(sessionState))
}
if(oldDeviceId != sessionState?.deviceId) {
_credentials.postValue(null) _credentials.postValue(null)
} }
} }
fun clearSession() {
_sessionState.postValue(ViewModelData.Empty)
_credentials.postValue(null)
}
private val _credentials = MutableLiveData<List<CredentialWithCode>?>() private val _credentials = MutableLiveData<List<CredentialWithCode>?>()
val credentials: LiveData<List<CredentialWithCode>?> = _credentials val credentials: LiveData<List<CredentialWithCode>?> = _credentials

View File

@ -24,6 +24,7 @@ import 'package:logging/logging.dart';
import '../../app/logging.dart'; import '../../app/logging.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../exception/cancellation_exception.dart'; import '../../exception/cancellation_exception.dart';
import '../../exception/no_data_exception.dart';
import '../../exception/platform_exception_decoder.dart'; import '../../exception/platform_exception_decoder.dart';
import '../../fido/models.dart'; import '../../fido/models.dart';
import '../../fido/state.dart'; import '../../fido/state.dart';
@ -33,19 +34,19 @@ final _log = Logger('android.fido.state');
const _methods = MethodChannel('android.fido.methods'); const _methods = MethodChannel('android.fido.methods');
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState?, DevicePath>(_FidoStateNotifier.new); .family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
class _FidoStateNotifier extends FidoStateNotifier { class _FidoStateNotifier extends FidoStateNotifier {
final _events = const EventChannel('android.fido.sessionState'); final _events = const EventChannel('android.fido.sessionState');
late StreamSubscription _sub; late StreamSubscription _sub;
@override @override
FutureOr<FidoState?> build(DevicePath devicePath) async { FutureOr<FidoState> build(DevicePath devicePath) async {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = const AsyncValue.data(null); state = AsyncValue.error(const NoDataException(), StackTrace.current);
} else if (json.containsKey('loading') && json['loading'] == true) { } else if (json == 'loading') {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
final fidoState = FidoState.fromJson(json); final fidoState = FidoState.fromJson(json);
@ -57,7 +58,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
ref.onDispose(_sub.cancel); ref.onDispose(_sub.cancel);
return Completer<FidoState?>().future; return Completer<FidoState>().future;
} }
@override @override
@ -191,8 +192,6 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = const AsyncValue.data(null);
} else if (json[0] is Map && json[0]['loading'] == true) {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
List<FidoCredential> newState = List.from( List<FidoCredential> newState = List.from(

View File

@ -24,13 +24,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/logging.dart'; import '../../app/logging.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../app/views/user_interaction.dart'; import '../../app/views/user_interaction.dart';
import '../../core/models.dart'; import '../../core/models.dart';
import '../../exception/cancellation_exception.dart'; import '../../exception/cancellation_exception.dart';
import '../../exception/no_data_exception.dart';
import '../../exception/platform_exception_decoder.dart'; import '../../exception/platform_exception_decoder.dart';
import '../../oath/models.dart'; import '../../oath/models.dart';
import '../../oath/state.dart'; import '../../oath/state.dart';
@ -52,8 +52,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = AsyncValue.error(const ErrorDataEmpty(), StackTrace.current); state = AsyncValue.error(const NoDataException(), StackTrace.current);
} else if (json[0] is Map && json[0]['loading'] == true) { } else if (json == 'loading') {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
final oathState = OathState.fromJson(json); final oathState = OathState.fromJson(json);

View File

@ -1,3 +0,0 @@
class ErrorDataEmpty {
const ErrorDataEmpty();
}

View File

@ -47,14 +47,14 @@ final _sessionProvider =
); );
final desktopFidoState = AsyncNotifierProvider.autoDispose final desktopFidoState = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState?, DevicePath>( .family<FidoStateNotifier, FidoState, DevicePath>(
_DesktopFidoStateNotifier.new); _DesktopFidoStateNotifier.new);
class _DesktopFidoStateNotifier extends FidoStateNotifier { class _DesktopFidoStateNotifier extends FidoStateNotifier {
late RpcNodeSession _session; late RpcNodeSession _session;
late StateController<String?> _pinController; late StateController<String?> _pinController;
FutureOr<FidoState?> _build(DevicePath devicePath) async { FutureOr<FidoState> _build(DevicePath devicePath) async {
var result = await _session.command('get'); var result = await _session.command('get');
FidoState fidoState = FidoState.fromJson(result['data']); FidoState fidoState = FidoState.fromJson(result['data']);
if (fidoState.hasPin && !fidoState.unlocked) { if (fidoState.hasPin && !fidoState.unlocked) {
@ -71,7 +71,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
} }
@override @override
FutureOr<FidoState?> build(DevicePath devicePath) async { FutureOr<FidoState> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath)); _session = ref.watch(_sessionProvider(devicePath));
if (Platform.isWindows) { if (Platform.isWindows) {
// Make sure to rebuild if isAdmin changes // Make sure to rebuild if isAdmin changes

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class NoDataException implements Exception {
const NoDataException();
}

View File

@ -21,11 +21,11 @@ import '../core/state.dart';
import 'models.dart'; import 'models.dart';
final fidoStateProvider = AsyncNotifierProvider.autoDispose final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState?, DevicePath>( .family<FidoStateNotifier, FidoState, DevicePath>(
() => throw UnimplementedError(), () => throw UnimplementedError(),
); );
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState?> { abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
Stream<InteractionEvent> reset(); Stream<InteractionEvent> reset();
Future<PinResult> setPin(String newPin, {String? oldPin}); Future<PinResult> setPin(String newPin, {String? oldPin});
Future<PinResult> unlock(String pin); Future<PinResult> unlock(String pin);

View File

@ -31,6 +31,7 @@ import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart'; import '../../app/views/message_page.dart';
import '../../app/views/message_page_not_initialized.dart'; import '../../app/views/message_page_not_initialized.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../exception/no_data_exception.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../../widgets/list_title.dart'; import '../../widgets/list_title.dart';
import '../features.dart' as features; import '../features.dart' as features;
@ -45,6 +46,7 @@ import 'pin_entry_form.dart';
class FingerprintsScreen extends ConsumerWidget { class FingerprintsScreen extends ConsumerWidget {
final YubiKeyData deviceData; final YubiKeyData deviceData;
const FingerprintsScreen(this.deviceData, {super.key}); const FingerprintsScreen(this.deviceData, {super.key});
@override @override
@ -57,6 +59,9 @@ class FingerprintsScreen extends ConsumerWidget {
builder: (context, _) => const CircularProgressIndicator(), builder: (context, _) => const CircularProgressIndicator(),
), ),
error: (error, _) { error: (error, _) {
if (error is NoDataException) {
return MessagePageNotInitialized(title: l10n.s_fingerprints);
}
final enabled = deviceData final enabled = deviceData
.info.config.enabledCapabilities[deviceData.node.transport] ?? .info.config.enabledCapabilities[deviceData.node.transport] ??
0; 0;
@ -74,9 +79,7 @@ class FingerprintsScreen extends ConsumerWidget {
); );
}, },
data: (fidoState) { data: (fidoState) {
return fidoState == null return fidoState.unlocked
? MessagePageNotInitialized(title: l10n.s_fingerprints)
: fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState) ? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState); : _FidoLockedPage(deviceData.node, fidoState);
}); });

View File

@ -32,6 +32,7 @@ import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart'; import '../../app/views/message_page.dart';
import '../../app/views/message_page_not_initialized.dart'; import '../../app/views/message_page_not_initialized.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../exception/no_data_exception.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../../widgets/list_title.dart'; import '../../widgets/list_title.dart';
import '../features.dart' as features; import '../features.dart' as features;
@ -58,6 +59,9 @@ class PasskeysScreen extends ConsumerWidget {
builder: (context, _) => const CircularProgressIndicator(), builder: (context, _) => const CircularProgressIndicator(),
), ),
error: (error, _) { error: (error, _) {
if (error is NoDataException) {
return MessagePageNotInitialized(title: l10n.s_passkeys);
}
final enabled = deviceData final enabled = deviceData
.info.config.enabledCapabilities[deviceData.node.transport] ?? .info.config.enabledCapabilities[deviceData.node.transport] ??
0; 0;
@ -76,9 +80,7 @@ class PasskeysScreen extends ConsumerWidget {
); );
}, },
data: (fidoState) { data: (fidoState) {
return fidoState == null return fidoState.unlocked
? MessagePageNotInitialized(title: l10n.s_passkeys)
: fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState) ? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState); : _FidoLockedPage(deviceData.node, fidoState);
}); });

View File

@ -23,7 +23,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/shortcuts.dart'; import '../../app/shortcuts.dart';
@ -34,6 +33,7 @@ import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart'; import '../../app/views/message_page.dart';
import '../../app/views/message_page_not_initialized.dart'; import '../../app/views/message_page_not_initialized.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../exception/no_data_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_form_field.dart'; import '../../widgets/app_text_form_field.dart';
@ -65,7 +65,7 @@ class OathScreen extends ConsumerWidget {
graphic: CircularProgressIndicator(), graphic: CircularProgressIndicator(),
delayedContent: true, delayedContent: true,
), ),
error: (error, _) => error is ErrorDataEmpty error: (error, _) => error is NoDataException
? MessagePageNotInitialized(title: l10n.s_accounts) ? MessagePageNotInitialized(title: l10n.s_accounts)
: AppFailurePage( : AppFailurePage(
cause: error, cause: error,