mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
use ViewModelData interface
This commit is contained in:
parent
ff8f2c8ce0
commit
9a19a9c608
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
class ErrorDataEmpty {
|
|
||||||
const ErrorDataEmpty();
|
|
||||||
}
|
|
@ -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
|
||||||
|
19
lib/exception/no_data_exception.dart
Normal file
19
lib/exception/no_data_exception.dart
Normal 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();
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user