first version of the feature, wip still

This commit is contained in:
Adam Velebil 2024-08-28 16:27:46 +02:00
parent 32d9cb1b39
commit d8a55a0297
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
27 changed files with 1830 additions and 546 deletions

View File

@ -22,17 +22,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
typealias OnDialogCancelled = suspend () -> Unit typealias OnDialogCancelled = suspend () -> Unit
enum class DialogTitle(val value: Int) {
TapKey(0),
OperationSuccessful(1),
OperationFailed(2)
}
class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) { class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
private val channel = private val channel =
MethodChannel(messenger, "com.yubico.authenticator.channel.dialog") MethodChannel(messenger, "com.yubico.authenticator.channel.dialog")
@ -48,40 +40,13 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
} }
} }
fun showDialog( fun showDialog(cancelled: OnDialogCancelled?) {
dialogTitle: DialogTitle,
dialogDescriptionId: Int,
cancelled: OnDialogCancelled?
) {
onCancelled = cancelled onCancelled = cancelled
coroutineScope.launch { coroutineScope.launch {
channel.invoke( channel.invoke("show", null)
"show",
Json.encodeToString(
mapOf(
"title" to dialogTitle.value,
"description" to dialogDescriptionId
)
)
)
} }
} }
suspend fun updateDialogState(
dialogTitle: DialogTitle,
dialogDescriptionId: Int? = null,
) {
channel.invoke(
"state",
Json.encodeToString(
mapOf(
"title" to dialogTitle.value,
"description" to dialogDescriptionId
)
)
)
}
suspend fun closeDialog() { suspend fun closeDialog() {
channel.invoke("close", NULL) channel.invoke("close", NULL)
} }

View File

@ -17,7 +17,6 @@
package com.yubico.authenticator package com.yubico.authenticator
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
@ -355,13 +354,14 @@ class MainActivity : FlutterFragmentActivity() {
try { try {
it.processYubiKey(device) it.processYubiKey(device)
if (device is NfcYubiKeyDevice) { if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
device.remove { device.remove {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY) appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
logger.error("Error processing YubiKey in AppContextManager", e) logger.error("Error processing YubiKey in AppContextManager", e)
} }
} }
} }
@ -441,6 +441,7 @@ class MainActivity : FlutterFragmentActivity() {
oathViewModel, oathViewModel,
dialogManager, dialogManager,
appPreferences, appPreferences,
appMethodChannel,
nfcActivityListener nfcActivityListener
) )

View File

@ -1,34 +0,0 @@
/*
* 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.
*/
package com.yubico.authenticator.fido
const val dialogDescriptionFidoIndex = 200
enum class FidoActionDescription(private val value: Int) {
Reset(0),
Unlock(1),
SetPin(2),
DeleteCredential(3),
DeleteFingerprint(4),
RenameFingerprint(5),
RegisterFingerprint(6),
EnableEnterpriseAttestation(7),
ActionFailure(8);
val id: Int
get() = value + dialogDescriptionFidoIndex
}

View File

@ -17,7 +17,6 @@
package com.yubico.authenticator.fido package com.yubico.authenticator.fido
import com.yubico.authenticator.DialogManager import com.yubico.authenticator.DialogManager
import com.yubico.authenticator.DialogTitle
import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.device.DeviceManager
import com.yubico.authenticator.fido.data.YubiKitFidoSession import com.yubico.authenticator.fido.data.YubiKitFidoSession
import com.yubico.authenticator.yubikit.withConnection import com.yubico.authenticator.yubikit.withConnection
@ -48,12 +47,9 @@ class FidoConnectionHelper(
} }
} }
suspend fun <T> useSession( suspend fun <T> useSession(action: (YubiKitFidoSession) -> T): T {
actionDescription: FidoActionDescription,
action: (YubiKitFidoSession) -> T
): T {
return deviceManager.withKey( return deviceManager.withKey(
onNfc = { useSessionNfc(actionDescription,action) }, onNfc = { useSessionNfc(action) },
onUsb = { useSessionUsb(it, action) }) onUsb = { useSessionUsb(it, action) })
} }
@ -64,10 +60,7 @@ class FidoConnectionHelper(
block(YubiKitFidoSession(it)) block(YubiKitFidoSession(it))
} }
suspend fun <T> useSessionNfc( suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): T {
actionDescription: FidoActionDescription,
block: (YubiKitFidoSession) -> T
): T {
try { try {
val result = suspendCoroutine { outer -> val result = suspendCoroutine { outer ->
pendingAction = { pendingAction = {
@ -75,11 +68,8 @@ class FidoConnectionHelper(
block.invoke(it.value) block.invoke(it.value)
}) })
} }
dialogManager.showDialog( dialogManager.showDialog {
DialogTitle.TapKey, logger.debug("Cancelled dialog")
actionDescription.id
) {
logger.debug("Cancelled Dialog {}", actionDescription.name)
pendingAction?.invoke(Result.failure(CancellationException())) pendingAction?.invoke(Result.failure(CancellationException()))
pendingAction = null pendingAction = null
} }

View File

@ -343,7 +343,7 @@ class FidoManager(
} }
private suspend fun unlock(pin: CharArray): String = private suspend fun unlock(pin: CharArray): String =
connectionHelper.useSession(FidoActionDescription.Unlock) { fidoSession -> connectionHelper.useSession { fidoSession ->
try { try {
val clientPin = val clientPin =
@ -380,7 +380,7 @@ class FidoManager(
} }
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String = private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
connectionHelper.useSession(FidoActionDescription.SetPin) { fidoSession -> connectionHelper.useSession { fidoSession ->
try { try {
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
@ -428,7 +428,7 @@ class FidoManager(
} }
private suspend fun deleteCredential(rpId: String, credentialId: String): String = private suspend fun deleteCredential(rpId: String, credentialId: String): String =
connectionHelper.useSession(FidoActionDescription.DeleteCredential) { fidoSession -> connectionHelper.useSession { fidoSession ->
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
@ -476,7 +476,7 @@ class FidoManager(
} }
private suspend fun deleteFingerprint(templateId: String): String = private suspend fun deleteFingerprint(templateId: String): String =
connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession -> connectionHelper.useSession { fidoSession ->
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
@ -501,7 +501,7 @@ class FidoManager(
} }
private suspend fun renameFingerprint(templateId: String, name: String): String = private suspend fun renameFingerprint(templateId: String, name: String): String =
connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession -> connectionHelper.useSession { fidoSession ->
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
@ -531,7 +531,7 @@ class FidoManager(
} }
private suspend fun registerFingerprint(name: String?): String = private suspend fun registerFingerprint(name: String?): String =
connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession -> connectionHelper.useSession { fidoSession ->
state?.cancel() state?.cancel()
state = CommandState() state = CommandState()
val clientPin = val clientPin =
@ -607,7 +607,7 @@ class FidoManager(
} }
private suspend fun enableEnterpriseAttestation(): String = private suspend fun enableEnterpriseAttestation(): String =
connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession -> connectionHelper.useSession { fidoSession ->
try { try {
val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo) val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)
val clientPin = ClientPin(fidoSession, uvAuthProtocol) val clientPin = ClientPin(fidoSession, uvAuthProtocol)

View File

@ -211,7 +211,7 @@ class FidoResetHelper(
coroutineScope.launch { coroutineScope.launch {
fidoViewModel.updateResetState(FidoResetState.Touch) fidoViewModel.updateResetState(FidoResetState.Touch)
try { try {
connectionHelper.useSessionNfc(FidoActionDescription.Reset) { fidoSession -> connectionHelper.useSessionNfc { fidoSession ->
doReset(fidoSession) doReset(fidoSession)
continuation.resume(Unit) continuation.resume(Unit)
} }

View File

@ -17,7 +17,6 @@
package com.yubico.authenticator.management package com.yubico.authenticator.management
import com.yubico.authenticator.DialogManager import com.yubico.authenticator.DialogManager
import com.yubico.authenticator.DialogTitle
import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.device.DeviceManager
import com.yubico.authenticator.yubikit.withConnection import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
@ -63,10 +62,7 @@ class ManagementConnectionHelper(
block.invoke(it.value) block.invoke(it.value)
}) })
} }
dialogManager.showDialog( dialogManager.showDialog {
DialogTitle.TapKey,
actionDescription.id
) {
logger.debug("Cancelled Dialog {}", actionDescription.name) logger.debug("Cancelled Dialog {}", actionDescription.name)
action?.invoke(Result.failure(CancellationException())) action?.invoke(Result.failure(CancellationException()))
action = null action = null

View File

@ -1,35 +0,0 @@
/*
* Copyright (C) 2023 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.
*/
package com.yubico.authenticator.oath
const val dialogDescriptionOathIndex = 100
enum class OathActionDescription(private val value: Int) {
Reset(0),
Unlock(1),
SetPassword(2),
UnsetPassword(3),
AddAccount(4),
RenameAccount(5),
DeleteAccount(6),
CalculateCode(7),
ActionFailure(8),
AddMultipleAccounts(9);
val id: Int
get() = value + dialogDescriptionOathIndex
}

View File

@ -58,6 +58,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardProtocol
import com.yubico.yubikit.core.util.Result import com.yubico.yubikit.core.util.Result
import com.yubico.yubikit.management.Capability import com.yubico.yubikit.management.Capability
import com.yubico.yubikit.oath.CredentialData import com.yubico.yubikit.oath.CredentialData
import com.yubico.yubikit.support.DeviceUtil
import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -65,6 +66,7 @@ import kotlinx.serialization.encodeToString
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URI import java.net.URI
import java.util.TimerTask
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -78,6 +80,7 @@ class OathManager(
private val oathViewModel: OathViewModel, private val oathViewModel: OathViewModel,
private val dialogManager: DialogManager, private val dialogManager: DialogManager,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
private val appMethodChannel: MainActivity.AppMethodChannel,
private val nfcActivityListener: NfcActivityListener private val nfcActivityListener: NfcActivityListener
) : AppContextManager(), DeviceListener { ) : AppContextManager(), DeviceListener {
@ -214,24 +217,33 @@ class OathManager(
coroutineScope.cancel() coroutineScope.cancel()
} }
var showProcessingTimerTask: TimerTask? = null
override suspend fun processYubiKey(device: YubiKeyDevice) { override suspend fun processYubiKey(device: YubiKeyDevice) {
try { try {
if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
}
device.withConnection<SmartCardConnection, Unit> { connection -> device.withConnection<SmartCardConnection, Unit> { connection ->
val session = getOathSession(connection) val session = getOathSession(connection)
val previousId = oathViewModel.currentSession()?.deviceId val previousId = oathViewModel.currentSession()?.deviceId
if (session.deviceId == previousId && device is NfcYubiKeyDevice) { if (session.deviceId == previousId && device is NfcYubiKeyDevice) {
// Run any pending action // Either run a pending action, or just refresh codes
pendingAction?.let { action -> if (pendingAction != null) {
action.invoke(Result.success(session)) pendingAction?.let { action ->
pendingAction = null action.invoke(Result.success(session))
} pendingAction = null
}
// Refresh codes } else {
if (!session.isLocked) { // Refresh codes
try { if (!session.isLocked) {
oathViewModel.updateCredentials(calculateOathCodes(session)) try {
} catch (error: Exception) { oathViewModel.updateCredentials(calculateOathCodes(session))
logger.error("Failed to refresh codes", error) } catch (error: Exception) {
logger.error("Failed to refresh codes", error)
throw error
}
} }
} }
} else { } else {
@ -261,6 +273,7 @@ class OathManager(
} else { } else {
// Awaiting an action for a different device? Fail it and stop processing. // Awaiting an action for a different device? Fail it and stop processing.
action.invoke(Result.failure(IllegalStateException("Wrong deviceId"))) action.invoke(Result.failure(IllegalStateException("Wrong deviceId")))
showProcessingTimerTask?.cancel()
return@withConnection return@withConnection
} }
} }
@ -281,11 +294,14 @@ class OathManager(
supportedCapabilities = oathCapabilities supportedCapabilities = oathCapabilities
) )
) )
showProcessingTimerTask?.cancel()
return@withConnection return@withConnection
} }
} }
} }
} }
showProcessingTimerTask?.cancel()
logger.debug( logger.debug(
"Successfully read Oath session info (and credentials if unlocked) from connected key" "Successfully read Oath session info (and credentials if unlocked) from connected key"
) )
@ -294,10 +310,12 @@ class OathManager(
deviceManager.setDeviceInfo(getDeviceInfo(device)) deviceManager.setDeviceInfo(getDeviceInfo(device))
} }
} catch (e: Exception) { } catch (e: Exception) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
// 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("Failed to connect to CCID: ", e) logger.error("Failed to connect to CCID: ", e)
// Clear any cached OATH state // Clear any cached OATH state
oathViewModel.clearSession() oathViewModel.clearSession()
throw e
} }
} }
@ -308,7 +326,7 @@ class OathManager(
val credentialData: CredentialData = val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri)) CredentialData.parseUri(URI.create(uri))
addToAny = true addToAny = true
return useOathSessionNfc(OathActionDescription.AddAccount) { session -> return useOathSessionNfc { session ->
// We need to check for duplicates here since we haven't yet read the credentials // We need to check for duplicates here since we haven't yet read the credentials
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
throw IllegalArgumentException() throw IllegalArgumentException()
@ -338,7 +356,7 @@ class OathManager(
logger.trace("Adding following accounts: {}", uris) logger.trace("Adding following accounts: {}", uris)
addToAny = true addToAny = true
return useOathSession(OathActionDescription.AddMultipleAccounts) { session -> return useOathSession { session ->
var successCount = 0 var successCount = 0
for (index in uris.indices) { for (index in uris.indices) {
@ -370,7 +388,7 @@ class OathManager(
} }
private suspend fun reset(): String = private suspend fun reset(): String =
useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) { useOathSession(updateDeviceInfo = true) {
// note, it is ok to reset locked session // note, it is ok to reset locked session
it.reset() it.reset()
keyManager.removeKey(it.deviceId) keyManager.removeKey(it.deviceId)
@ -382,7 +400,7 @@ class OathManager(
} }
private suspend fun unlock(password: String, remember: Boolean): String = private suspend fun unlock(password: String, remember: Boolean): String =
useOathSession(OathActionDescription.Unlock) { useOathSession {
val accessKey = it.deriveAccessKey(password.toCharArray()) val accessKey = it.deriveAccessKey(password.toCharArray())
keyManager.addKey(it.deviceId, accessKey, remember) keyManager.addKey(it.deviceId, accessKey, remember)
@ -390,11 +408,7 @@ class OathManager(
val remembered = keyManager.isRemembered(it.deviceId) val remembered = keyManager.isRemembered(it.deviceId)
if (unlocked) { if (unlocked) {
oathViewModel.setSessionState(Session(it, remembered)) oathViewModel.setSessionState(Session(it, remembered))
oathViewModel.updateCredentials(calculateOathCodes(it))
// fetch credentials after unlocking only if the YubiKey is connected over USB
if (deviceManager.isUsbKeyConnected()) {
oathViewModel.updateCredentials(calculateOathCodes(it))
}
} }
jsonSerializer.encodeToString(mapOf("unlocked" to unlocked, "remembered" to remembered)) jsonSerializer.encodeToString(mapOf("unlocked" to unlocked, "remembered" to remembered))
@ -405,7 +419,6 @@ class OathManager(
newPassword: String, newPassword: String,
): String = ): String =
useOathSession( useOathSession(
OathActionDescription.SetPassword,
unlock = false, unlock = false,
updateDeviceInfo = true updateDeviceInfo = true
) { session -> ) { session ->
@ -427,7 +440,7 @@ class OathManager(
} }
private suspend fun unsetPassword(currentPassword: String): String = private suspend fun unsetPassword(currentPassword: String): String =
useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session -> useOathSession(unlock = false) { session ->
if (session.isAccessKeySet) { if (session.isAccessKeySet) {
// test current password sent by the user // test current password sent by the user
if (session.unlock(currentPassword.toCharArray())) { if (session.unlock(currentPassword.toCharArray())) {
@ -459,7 +472,7 @@ class OathManager(
uri: String, uri: String,
requireTouch: Boolean, requireTouch: Boolean,
): String = ): String =
useOathSession(OathActionDescription.AddAccount) { session -> useOathSession { session ->
val credentialData: CredentialData = val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri)) CredentialData.parseUri(URI.create(uri))
@ -480,21 +493,30 @@ class OathManager(
} }
private suspend fun renameAccount(uri: String, name: String, issuer: String?): String = private suspend fun renameAccount(uri: String, name: String, issuer: String?): String =
useOathSession(OathActionDescription.RenameAccount) { session -> useOathSession { session ->
val credential = getOathCredential(session, uri) val credential = getCredential(uri)
val renamedCredential = val renamed = Credential(
Credential(session.renameCredential(credential, name, issuer), session.deviceId) session.renameCredential(credential, name, issuer),
oathViewModel.renameCredential( session.deviceId
Credential(credential, session.deviceId),
renamedCredential
) )
jsonSerializer.encodeToString(renamedCredential) oathViewModel.renameCredential(
Credential(credential, session.deviceId),
renamed
)
// // simulate long taking op
// val renamedCredential = credential
// logger.debug("simulate error")
// Thread.sleep(3000)
// throw IOException("Test exception")
jsonSerializer.encodeToString(renamed)
} }
private suspend fun deleteAccount(credentialId: String): String = private suspend fun deleteAccount(credentialId: String): String =
useOathSession(OathActionDescription.DeleteAccount) { session -> useOathSession { session ->
val credential = getOathCredential(session, credentialId) val credential = getCredential(credentialId)
session.deleteCredential(credential) session.deleteCredential(credential)
oathViewModel.removeCredential(Credential(credential, session.deviceId)) oathViewModel.removeCredential(Credential(credential, session.deviceId))
NULL NULL
@ -546,8 +568,8 @@ class OathManager(
private suspend fun calculate(credentialId: String): String = private suspend fun calculate(credentialId: String): String =
useOathSession(OathActionDescription.CalculateCode) { session -> useOathSession { session ->
val credential = getOathCredential(session, credentialId) val credential = getCredential(credentialId)
val code = Code.from(calculateCode(session, credential)) val code = Code.from(calculateCode(session, credential))
oathViewModel.updateCode( oathViewModel.updateCode(
@ -649,31 +671,43 @@ class OathManager(
return session.calculateCodes(timestamp).map { (credential, code) -> return session.calculateCodes(timestamp).map { (credential, code) ->
Pair( Pair(
Credential(credential, session.deviceId), Credential(credential, session.deviceId),
Code.from(if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) { Code.from(
session.calculateSteamCode(credential, timestamp) if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) {
} else if (credential.isTouchRequired && bypassTouch) { session.calculateSteamCode(credential, timestamp)
session.calculateCode(credential, timestamp) } else if (credential.isTouchRequired && bypassTouch) {
} else { session.calculateCode(credential, timestamp)
code } else {
}) code
}
)
) )
}.toMap() }.toMap()
} }
private fun getCredential(id: String): YubiKitCredential {
val credential =
oathViewModel.credentials.value?.find { it.credential.id == id }?.credential
if (credential == null || credential.data == null) {
logger.debug("Failed to find credential with id: {}", id)
throw Exception("Failed to find account")
}
return credential.data
}
private suspend fun <T> useOathSession( private suspend fun <T> useOathSession(
oathActionDescription: OathActionDescription,
unlock: Boolean = true, unlock: Boolean = true,
updateDeviceInfo: Boolean = false, updateDeviceInfo: Boolean = false,
action: (YubiKitOathSession) -> T action: (YubiKitOathSession) -> T
): T { ): T {
// callers can decide whether the session should be unlocked first // callers can decide whether the session should be unlocked first
unlockOnConnect.set(unlock) unlockOnConnect.set(unlock)
// callers can request whether device info should be updated after session operation // callers can request whether device info should be updated after session operation
this@OathManager.updateDeviceInfo.set(updateDeviceInfo) this@OathManager.updateDeviceInfo.set(updateDeviceInfo)
return deviceManager.withKey( return deviceManager.withKey(
onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) }, onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) },
onNfc = { useOathSessionNfc(oathActionDescription, action) } onNfc = { useOathSessionNfc(action) }
) )
} }
@ -690,50 +724,42 @@ class OathManager(
} }
private suspend fun <T> useOathSessionNfc( private suspend fun <T> useOathSessionNfc(
oathActionDescription: OathActionDescription,
block: (YubiKitOathSession) -> T block: (YubiKitOathSession) -> T
): T { ): T {
try { var firstShow = true
val result = suspendCoroutine { outer -> while (true) { // loop until success or cancel
pendingAction = { try {
outer.resumeWith(runCatching { val result = suspendCoroutine { outer ->
block.invoke(it.value) pendingAction = {
}) outer.resumeWith(runCatching {
} val session = it.value // this can throw CancellationException
dialogManager.showDialog(DialogTitle.TapKey, oathActionDescription.id) { nfcActivityListener.onChange(NfcActivityState.PROCESSING_STARTED)
logger.debug("Cancelled Dialog {}", oathActionDescription.name) block.invoke(session)
pendingAction?.invoke(Result.failure(CancellationException())) })
pendingAction = null }
}
}
nfcActivityListener.onChange(NfcActivityState.PROCESSING_FINISHED)
dialogManager.updateDialogState(
dialogTitle = DialogTitle.OperationSuccessful
)
// TODO: This delays the closing of the dialog, but also the return value
delay(1500)
return result
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Throwable) {
nfcActivityListener.onChange(NfcActivityState.PROCESSING_INTERRUPTED)
dialogManager.updateDialogState(
dialogTitle = DialogTitle.OperationFailed,
dialogDescriptionId = OathActionDescription.ActionFailure.id
)
// TODO: This delays the closing of the dialog, but also the return value
delay(1500)
throw error
} finally {
dialogManager.closeDialog()
}
}
private fun getOathCredential(session: YubiKitOathSession, credentialId: String) = if (firstShow) {
// we need to use oathSession.calculateCodes() to get proper Credential.touchRequired value dialogManager.showDialog {
session.calculateCodes().map { e -> e.key }.firstOrNull { credential -> logger.debug("Cancelled dialog")
(credential != null) && credential.id.asString() == credentialId pendingAction?.invoke(Result.failure(CancellationException()))
} ?: throw Exception("Failed to find account") pendingAction = null
}
firstShow = false
}
// here the coroutine is suspended and waits till pendingAction is
// invoked - the pending action result will resume this coroutine
}
nfcActivityListener.onChange(NfcActivityState.PROCESSING_FINISHED)
return result
} catch (cancelled: CancellationException) {
throw cancelled
} catch (e: Exception) {
logger.error("Exception during action: ", e)
nfcActivityListener.onChange(NfcActivityState.PROCESSING_INTERRUPTED)
throw e
}
} // while
}
override fun onConnected(device: YubiKeyDevice) { override fun onConnected(device: YubiKeyDevice) {
refreshJob?.cancel() refreshJob?.cancel()

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 Yubico. * Copyright (C) 2023-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.
@ -35,9 +35,10 @@ data class Credential(
@SerialName("name") @SerialName("name")
val accountName: String, val accountName: String,
@SerialName("touch_required") @SerialName("touch_required")
val touchRequired: Boolean val touchRequired: Boolean,
@kotlinx.serialization.Transient
val data: YubiKitCredential? = null
) { ) {
constructor(credential: YubiKitCredential, deviceId: String) : this( constructor(credential: YubiKitCredential, deviceId: String) : this(
deviceId = deviceId, deviceId = deviceId,
id = credential.id.asString(), id = credential.id.asString(),
@ -48,7 +49,8 @@ data class Credential(
period = credential.period, period = credential.period,
issuer = credential.issuer, issuer = credential.issuer,
accountName = credential.accountName, accountName = credential.accountName,
touchRequired = credential.isTouchRequired touchRequired = credential.isTouchRequired,
data = credential
) )
override fun equals(other: Any?): Boolean = override fun equals(other: Any?): Boolean =

View File

@ -19,6 +19,7 @@ package com.yubico.authenticator.yubikit
import android.app.Activity import android.app.Activity
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.Tag import android.nfc.Tag
import com.yubico.authenticator.yubikit.NfcActivityListener
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
import com.yubico.yubikit.android.transport.nfc.NfcDispatcher import com.yubico.yubikit.android.transport.nfc.NfcDispatcher
@ -51,7 +52,7 @@ class NfcActivityDispatcher(private val listener: NfcActivityListener) : NfcDisp
nfcConfiguration, nfcConfiguration,
TagInterceptor(listener, handler) TagInterceptor(listener, handler)
) )
listener.onChange(NfcActivityState.READY) //listener.onChange(NfcActivityState.READY)
} }
override fun disable(activity: Activity) { override fun disable(activity: Activity) {
@ -68,7 +69,7 @@ class NfcActivityDispatcher(private val listener: NfcActivityListener) : NfcDisp
private val logger = LoggerFactory.getLogger(TagInterceptor::class.java) private val logger = LoggerFactory.getLogger(TagInterceptor::class.java)
override fun onTag(tag: Tag) { override fun onTag(tag: Tag) {
listener.onChange(NfcActivityState.PROCESSING_STARTED) //listener.onChange(NfcActivityState.PROCESSING_STARTED)
logger.debug("forwarding tag") logger.debug("forwarding tag")
tagHandler.onTag(tag) tagHandler.onTag(tag)
} }

View File

@ -26,7 +26,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.5.0" apply false id "com.android.application" version '8.5.2' apply false
id "org.jetbrains.kotlin.android" version "2.0.0" apply false id "org.jetbrains.kotlin.android" version "2.0.0" apply false
id "org.jetbrains.kotlin.plugin.serialization" version "2.0.0" apply false id "org.jetbrains.kotlin.plugin.serialization" version "2.0.0" apply false
id "com.google.android.gms.oss-licenses-plugin" version "0.10.6" apply false id "com.google.android.gms.oss-licenses-plugin" version "0.10.6" apply false

View File

@ -32,17 +32,18 @@ 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';
import '../tap_request_dialog.dart';
final _log = Logger('android.fido.state'); final _log = Logger('android.fido.state');
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;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override @override
FutureOr<FidoState> build(DevicePath devicePath) async { FutureOr<FidoState> build(DevicePath devicePath) async {
@ -79,7 +80,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
}); });
controller.onCancel = () async { controller.onCancel = () async {
await _methods.invokeMethod('cancelReset'); await fido.cancelReset();
if (!controller.isClosed) { if (!controller.isClosed) {
await subscription.cancel(); await subscription.cancel();
} }
@ -87,7 +88,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
controller.onListen = () async { controller.onListen = () async {
try { try {
await _methods.invokeMethod('reset'); await fido.reset();
await controller.sink.close(); await controller.sink.close();
ref.invalidateSelf(); ref.invalidateSelf();
} catch (e) { } catch (e) {
@ -102,13 +103,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override @override
Future<PinResult> setPin(String newPin, {String? oldPin}) async { Future<PinResult> setPin(String newPin, {String? oldPin}) async {
try { try {
final response = jsonDecode(await _methods.invokeMethod( final response = jsonDecode(await fido.setPin(newPin, oldPin: oldPin));
'setPin',
{
'pin': oldPin,
'newPin': newPin,
},
));
if (response['success'] == true) { if (response['success'] == true) {
_log.debug('FIDO PIN set/change successful'); _log.debug('FIDO PIN set/change successful');
return PinResult.success(); return PinResult.success();
@ -134,10 +129,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override @override
Future<PinResult> unlock(String pin) async { Future<PinResult> unlock(String pin) async {
try { try {
final response = jsonDecode(await _methods.invokeMethod( final response = jsonDecode(await fido.unlock(pin));
'unlock',
{'pin': pin},
));
if (response['success'] == true) { if (response['success'] == true) {
_log.debug('FIDO applet unlocked'); _log.debug('FIDO applet unlocked');
@ -165,9 +157,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override @override
Future<void> enableEnterpriseAttestation() async { Future<void> enableEnterpriseAttestation() async {
try { try {
final response = jsonDecode(await _methods.invokeMethod( final response = jsonDecode(await fido.enableEnterpriseAttestation());
'enableEnterpriseAttestation',
));
if (response['success'] == true) { if (response['success'] == true) {
_log.debug('Enterprise attestation enabled'); _log.debug('Enterprise attestation enabled');
@ -193,6 +183,8 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
final _events = const EventChannel('android.fido.fingerprints'); final _events = const EventChannel('android.fido.fingerprints');
late StreamSubscription _sub; late StreamSubscription _sub;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override @override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async { FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
@ -243,15 +235,14 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
controller.onCancel = () async { controller.onCancel = () async {
if (!controller.isClosed) { if (!controller.isClosed) {
_log.debug('Cancelling fingerprint registration'); _log.debug('Cancelling fingerprint registration');
await _methods.invokeMethod('cancelRegisterFingerprint'); await fido.cancelFingerprintRegistration();
await registerFpSub.cancel(); await registerFpSub.cancel();
} }
}; };
controller.onListen = () async { controller.onListen = () async {
try { try {
final registerFpResult = final registerFpResult = await fido.registerFingerprint(name);
await _methods.invokeMethod('registerFingerprint', {'name': name});
_log.debug('Finished registerFingerprint with: $registerFpResult'); _log.debug('Finished registerFingerprint with: $registerFpResult');
@ -286,13 +277,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
Future<Fingerprint> renameFingerprint( Future<Fingerprint> renameFingerprint(
Fingerprint fingerprint, String name) async { Fingerprint fingerprint, String name) async {
try { try {
final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod( final renameFingerprintResponse =
'renameFingerprint', jsonDecode(await fido.renameFingerprint(fingerprint, name));
{
'templateId': fingerprint.templateId,
'name': name,
},
));
if (renameFingerprintResponse['success'] == true) { if (renameFingerprintResponse['success'] == true) {
_log.debug('FIDO rename fingerprint succeeded'); _log.debug('FIDO rename fingerprint succeeded');
@ -316,12 +302,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
@override @override
Future<void> deleteFingerprint(Fingerprint fingerprint) async { Future<void> deleteFingerprint(Fingerprint fingerprint) async {
try { try {
final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod( final deleteFingerprintResponse =
'deleteFingerprint', jsonDecode(await fido.deleteFingerprint(fingerprint));
{
'templateId': fingerprint.templateId,
},
));
if (deleteFingerprintResponse['success'] == true) { if (deleteFingerprintResponse['success'] == true) {
_log.debug('FIDO delete fingerprint succeeded'); _log.debug('FIDO delete fingerprint succeeded');
@ -348,6 +330,8 @@ final androidCredentialProvider = AsyncNotifierProvider.autoDispose
class _FidoCredentialsNotifier extends FidoCredentialsNotifier { class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
final _events = const EventChannel('android.fido.credentials'); final _events = const EventChannel('android.fido.credentials');
late StreamSubscription _sub; late StreamSubscription _sub;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override @override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async { FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
@ -371,13 +355,7 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
@override @override
Future<void> deleteCredential(FidoCredential credential) async { Future<void> deleteCredential(FidoCredential credential) async {
try { try {
await _methods.invokeMethod( await fido.deleteCredential(credential);
'deleteCredential',
{
'rpId': credential.rpId,
'credentialId': credential.credentialId,
},
);
} on PlatformException catch (pe) { } on PlatformException catch (pe) {
var decodedException = pe.decode(); var decodedException = pe.decode();
if (decodedException is CancellationException) { if (decodedException is CancellationException) {
@ -388,3 +366,88 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
} }
} }
} }
final _fidoMethodsProvider = NotifierProvider<_FidoMethodChannelNotifier, void>(
() => _FidoMethodChannelNotifier());
class _FidoMethodChannelNotifier extends MethodChannelNotifier {
_FidoMethodChannelNotifier()
: super(const MethodChannel('android.fido.methods'));
late final l10n = ref.read(l10nProvider);
@override
void build() {}
Future<dynamic> deleteCredential(FidoCredential credential) async =>
invoke('deleteCredential', {
'callArgs': {
'rpId': credential.rpId,
'credentialId': credential.credentialId
},
'operationName': l10n.s_nfc_dialog_fido_delete_credential,
'operationProcessing':
l10n.s_nfc_dialog_fido_delete_credential_processing,
'operationSuccess': l10n.s_nfc_dialog_fido_delete_credential_success,
'operationFailure': l10n.s_nfc_dialog_fido_delete_credential_failure,
'showSuccess': true
});
Future<dynamic> cancelReset() async => invoke('cancelReset');
Future<dynamic> reset() async => invoke('reset', {
'operationName': l10n.s_nfc_dialog_fido_reset,
'operationProcessing': l10n.s_nfc_dialog_fido_reset_processing,
'operationSuccess': l10n.s_nfc_dialog_fido_reset_success,
'operationFailure': l10n.s_nfc_dialog_fido_reset_failure,
'showSuccess': true
});
Future<dynamic> setPin(String newPin, {String? oldPin}) async =>
invoke('setPin', {
'callArgs': {'pin': oldPin, 'newPin': newPin},
'operationName': oldPin != null
? l10n.s_nfc_dialog_fido_change_pin
: l10n.s_nfc_dialog_fido_set_pin,
'operationProcessing': oldPin != null
? l10n.s_nfc_dialog_fido_change_pin_processing
: l10n.s_nfc_dialog_fido_set_pin_processing,
'operationSuccess': oldPin != null
? l10n.s_nfc_dialog_fido_change_pin_success
: l10n.s_nfc_dialog_fido_set_pin_success,
'operationFailure': oldPin != null
? l10n.s_nfc_dialog_fido_change_pin_failure
: l10n.s_nfc_dialog_fido_set_pin_failure,
'showSuccess': true
});
Future<dynamic> unlock(String pin) async => invoke('unlock', {
'callArgs': {'pin': pin},
'operationName': l10n.s_nfc_dialog_fido_unlock,
'operationProcessing': l10n.s_nfc_dialog_fido_unlock_processing,
'operationSuccess': l10n.s_nfc_dialog_fido_unlock_success,
'operationFailure': l10n.s_nfc_dialog_fido_unlock_failure,
'showSuccess': true
});
Future<dynamic> enableEnterpriseAttestation() async =>
invoke('enableEnterpriseAttestation');
Future<dynamic> registerFingerprint(String? name) async =>
invoke('registerFingerprint', {
'callArgs': {'name': name}
});
Future<dynamic> cancelFingerprintRegistration() async =>
invoke('cancelRegisterFingerprint');
Future<dynamic> renameFingerprint(
Fingerprint fingerprint, String name) async =>
invoke('renameFingerprint', {
'callArgs': {'templateId': fingerprint.templateId, 'name': name},
});
Future<dynamic> deleteFingerprint(Fingerprint fingerprint) async =>
invoke('deleteFingerprint', {
'callArgs': {'templateId': fingerprint.templateId},
});
}

View File

@ -43,6 +43,7 @@ import 'oath/state.dart';
import 'qr_scanner/qr_scanner_provider.dart'; import 'qr_scanner/qr_scanner_provider.dart';
import 'state.dart'; import 'state.dart';
import 'tap_request_dialog.dart'; import 'tap_request_dialog.dart';
import 'views/nfc/nfc_activity_command_listener.dart';
import 'window_state_provider.dart'; import 'window_state_provider.dart';
Future<Widget> initialize() async { Future<Widget> initialize() async {
@ -106,6 +107,8 @@ Future<Widget> initialize() async {
child: DismissKeyboard( child: DismissKeyboard(
child: YubicoAuthenticatorApp(page: Consumer( child: YubicoAuthenticatorApp(page: Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
ref.read(nfcActivityCommandListener).startListener(context);
Timer.run(() { Timer.run(() {
ref.read(featureFlagProvider.notifier) ref.read(featureFlagProvider.notifier)
// TODO: Load feature flags from file/config? // TODO: Load feature flags from file/config?

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.
@ -35,11 +35,10 @@ 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';
import '../tap_request_dialog.dart';
final _log = Logger('android.oath.state'); final _log = Logger('android.oath.state');
const _methods = MethodChannel('android.oath.methods');
final androidOathStateProvider = AsyncNotifierProvider.autoDispose final androidOathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>( .family<OathStateNotifier, OathState, DevicePath>(
_AndroidOathStateNotifier.new); _AndroidOathStateNotifier.new);
@ -47,6 +46,8 @@ final androidOathStateProvider = AsyncNotifierProvider.autoDispose
class _AndroidOathStateNotifier extends OathStateNotifier { class _AndroidOathStateNotifier extends OathStateNotifier {
final _events = const EventChannel('android.oath.sessionState'); final _events = const EventChannel('android.oath.sessionState');
late StreamSubscription _sub; late StreamSubscription _sub;
late _OathMethodChannelNotifier oath =
ref.watch(_oathMethodsProvider.notifier);
@override @override
FutureOr<OathState> build(DevicePath arg) { FutureOr<OathState> build(DevicePath arg) {
@ -75,7 +76,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
// await ref // await ref
// .read(androidAppContextHandler) // .read(androidAppContextHandler)
// .switchAppContext(Application.accounts); // .switchAppContext(Application.accounts);
await _methods.invokeMethod('reset'); await oath.reset();
} catch (e) { } catch (e) {
_log.debug('Calling reset failed with exception: $e'); _log.debug('Calling reset failed with exception: $e');
} }
@ -84,8 +85,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override @override
Future<(bool, bool)> unlock(String password, {bool remember = false}) async { Future<(bool, bool)> unlock(String password, {bool remember = false}) async {
try { try {
final unlockResponse = jsonDecode(await _methods.invokeMethod( final unlockResponse =
'unlock', {'password': password, 'remember': remember})); jsonDecode(await oath.unlock(password, remember: remember));
_log.debug('applet unlocked'); _log.debug('applet unlocked');
final unlocked = unlockResponse['unlocked'] == true; final unlocked = unlockResponse['unlocked'] == true;
@ -106,8 +107,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override @override
Future<bool> setPassword(String? current, String password) async { Future<bool> setPassword(String? current, String password) async {
try { try {
await _methods.invokeMethod( await oath.setPassword(current, password);
'setPassword', {'current': current, 'password': password});
return true; return true;
} on PlatformException catch (e) { } on PlatformException catch (e) {
_log.debug('Calling set password failed with exception: $e'); _log.debug('Calling set password failed with exception: $e');
@ -118,7 +118,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override @override
Future<bool> unsetPassword(String current) async { Future<bool> unsetPassword(String current) async {
try { try {
await _methods.invokeMethod('unsetPassword', {'current': current}); await oath.unsetPassword(current);
return true; return true;
} on PlatformException catch (e) { } on PlatformException catch (e) {
_log.debug('Calling unset password failed with exception: $e'); _log.debug('Calling unset password failed with exception: $e');
@ -129,7 +129,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override @override
Future<void> forgetPassword() async { Future<void> forgetPassword() async {
try { try {
await _methods.invokeMethod('forgetPassword'); await oath.forgetPassword();
} on PlatformException catch (e) { } on PlatformException catch (e) {
_log.debug('Calling forgetPassword failed with exception: $e'); _log.debug('Calling forgetPassword failed with exception: $e');
} }
@ -161,12 +161,10 @@ Exception _decodeAddAccountException(PlatformException platformException) {
final addCredentialToAnyProvider = final addCredentialToAnyProvider =
Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async { Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async {
final oath = ref.watch(_oathMethodsProvider.notifier);
try { try {
String resultString = await _methods.invokeMethod( String resultString = await oath.addAccountToAny(credentialUri,
'addAccountToAny', { requireTouch: requireTouch);
'uri': credentialUri.toString(),
'requireTouch': requireTouch
});
var result = jsonDecode(resultString); var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']); return OathCredential.fromJson(result['credential']);
@ -177,17 +175,13 @@ final addCredentialToAnyProvider =
final addCredentialsToAnyProvider = Provider( final addCredentialsToAnyProvider = Provider(
(ref) => (List<String> credentialUris, List<bool> touchRequired) async { (ref) => (List<String> credentialUris, List<bool> touchRequired) async {
final oath = ref.read(_oathMethodsProvider.notifier);
try { try {
_log.debug( _log.debug(
'Calling android with ${credentialUris.length} credentials to be added'); 'Calling android with ${credentialUris.length} credentials to be added');
String resultString = await _methods.invokeMethod( String resultString =
'addAccountsToAny', await oath.addAccounts(credentialUris, touchRequired);
{
'uris': credentialUris,
'requireTouch': touchRequired,
},
);
_log.debug('Call result: $resultString'); _log.debug('Call result: $resultString');
var result = jsonDecode(resultString); var result = jsonDecode(resultString);
@ -218,6 +212,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
final WithContext _withContext; final WithContext _withContext;
final Ref _ref; final Ref _ref;
late StreamSubscription _sub; late StreamSubscription _sub;
late _OathMethodChannelNotifier oath =
_ref.read(_oathMethodsProvider.notifier);
_AndroidCredentialListNotifier(this._withContext, this._ref) : super() { _AndroidCredentialListNotifier(this._withContext, this._ref) : super() {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
@ -264,8 +260,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
} }
try { try {
final resultJson = await _methods final resultJson = await oath.calculate(credential);
.invokeMethod('calculate', {'credentialId': credential.id});
_log.debug('Calculate', resultJson); _log.debug('Calculate', resultJson);
return OathCode.fromJson(jsonDecode(resultJson)); return OathCode.fromJson(jsonDecode(resultJson));
} on PlatformException catch (pe) { } on PlatformException catch (pe) {
@ -280,9 +275,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
Future<OathCredential> addAccount(Uri credentialUri, Future<OathCredential> addAccount(Uri credentialUri,
{bool requireTouch = false}) async { {bool requireTouch = false}) async {
try { try {
String resultString = await _methods.invokeMethod('addAccount', String resultString =
{'uri': credentialUri.toString(), 'requireTouch': requireTouch}); await oath.addAccount(credentialUri, requireTouch: requireTouch);
var result = jsonDecode(resultString); var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']); return OathCredential.fromJson(result['credential']);
} on PlatformException catch (pe) { } on PlatformException catch (pe) {
@ -294,9 +288,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
Future<OathCredential> renameAccount( Future<OathCredential> renameAccount(
OathCredential credential, String? issuer, String name) async { OathCredential credential, String? issuer, String name) async {
try { try {
final response = await _methods.invokeMethod('renameAccount', final response = await oath.renameAccount(credential, issuer, name);
{'credentialId': credential.id, 'name': name, 'issuer': issuer});
_log.debug('Rename response: $response'); _log.debug('Rename response: $response');
var responseJson = jsonDecode(response); var responseJson = jsonDecode(response);
@ -311,11 +303,149 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
@override @override
Future<void> deleteAccount(OathCredential credential) async { Future<void> deleteAccount(OathCredential credential) async {
try { try {
await _methods await oath.deleteAccount(credential);
.invokeMethod('deleteAccount', {'credentialId': credential.id});
} on PlatformException catch (e) { } on PlatformException catch (e) {
_log.debug('Received exception: $e'); var decoded = e.decode();
throw e.decode(); if (decoded is CancellationException) {
_log.debug('Account delete was cancelled.');
} else {
_log.debug('Received exception: $e');
}
throw decoded;
} }
} }
} }
final _oathMethodsProvider = NotifierProvider<_OathMethodChannelNotifier, void>(
() => _OathMethodChannelNotifier());
class _OathMethodChannelNotifier extends MethodChannelNotifier {
_OathMethodChannelNotifier()
: super(const MethodChannel('android.oath.methods'));
late final l10n = ref.read(l10nProvider);
@override
void build() {}
Future<dynamic> reset() async => invoke('reset', {
'operationName': l10n.s_nfc_dialog_oath_reset,
'operationProcessing': l10n.s_nfc_dialog_oath_reset_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_reset_success,
'operationFailure': l10n.s_nfc_dialog_oath_reset_failure
});
Future<dynamic> unlock(String password, {bool remember = false}) async =>
invoke('unlock', {
'callArgs': {'password': password, 'remember': remember},
'operationName': l10n.s_nfc_dialog_oath_unlock,
'operationProcessing': l10n.s_nfc_dialog_oath_unlock_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_unlock_success,
'operationFailure': l10n.s_nfc_dialog_oath_unlock_failure,
});
Future<dynamic> setPassword(String? current, String password) async =>
invoke('setPassword', {
'callArgs': {'current': current, 'password': password},
'operationName': current != null
? l10n.s_nfc_dialog_oath_change_password
: l10n.s_nfc_dialog_oath_set_password,
'operationProcessing': current != null
? l10n.s_nfc_dialog_oath_change_password_processing
: l10n.s_nfc_dialog_oath_set_password_processing,
'operationSuccess': current != null
? l10n.s_nfc_dialog_oath_change_password_success
: l10n.s_nfc_dialog_oath_set_password_success,
'operationFailure': current != null
? l10n.s_nfc_dialog_oath_change_password_failure
: l10n.s_nfc_dialog_oath_set_password_failure,
});
Future<dynamic> unsetPassword(String current) async =>
invoke('unsetPassword', {
'callArgs': {'current': current},
'operationName': l10n.s_nfc_dialog_oath_remove_password,
'operationProcessing':
l10n.s_nfc_dialog_oath_remove_password_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_remove_password_success,
'operationFailure': l10n.s_nfc_dialog_oath_remove_password_failure,
});
Future<dynamic> forgetPassword() async => invoke('forgetPassword');
Future<dynamic> calculate(OathCredential credential) async =>
invoke('calculate', {
'callArgs': {'credentialId': credential.id},
'operationName': l10n.s_nfc_dialog_oath_calculate_code,
'operationProcessing': l10n.s_nfc_dialog_oath_calculate_code_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_calculate_code_success,
'operationFailure': l10n.s_nfc_dialog_oath_calculate_code_failure,
});
Future<dynamic> addAccount(Uri credentialUri,
{bool requireTouch = false}) async =>
invoke('addAccount', {
'callArgs': {
'uri': credentialUri.toString(),
'requireTouch': requireTouch
},
'operationName': l10n.s_nfc_dialog_oath_add_account,
'operationProcessing': l10n.s_nfc_dialog_oath_add_account_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_add_account_success,
'operationFailure': l10n.s_nfc_dialog_oath_add_account_failure,
'showSuccess': true
});
Future<dynamic> addAccounts(
List<String> credentialUris, List<bool> touchRequired) async =>
invoke('addAccountsToAny', {
'callArgs': {
'uris': credentialUris,
'requireTouch': touchRequired,
},
'operationName': l10n.s_nfc_dialog_oath_add_multiple_accounts,
'operationProcessing':
l10n.s_nfc_dialog_oath_add_multiple_accounts_processing,
'operationSuccess':
l10n.s_nfc_dialog_oath_add_multiple_accounts_success,
'operationFailure':
l10n.s_nfc_dialog_oath_add_multiple_accounts_failure,
});
Future<dynamic> addAccountToAny(Uri credentialUri,
{bool requireTouch = false}) async =>
invoke('addAccountToAny', {
'callArgs': {
'uri': credentialUri.toString(),
'requireTouch': requireTouch
},
'operationName': l10n.s_nfc_dialog_oath_add_account,
'operationProcessing': l10n.s_nfc_dialog_oath_add_account_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_add_account_success,
'operationFailure': l10n.s_nfc_dialog_oath_add_account_failure,
});
Future<dynamic> deleteAccount(OathCredential credential) async =>
invoke('deleteAccount', {
'callArgs': {'credentialId': credential.id},
'operationName': l10n.s_nfc_dialog_oath_delete_account,
'operationProcessing': l10n.s_nfc_dialog_oath_delete_account_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_delete_account_success,
'operationFailure': l10n.s_nfc_dialog_oath_delete_account_failure,
'showSuccess': true
});
Future<dynamic> renameAccount(
OathCredential credential, String? issuer, String name) async =>
invoke('renameAccount', {
'callArgs': {
'credentialId': credential.id,
'name': name,
'issuer': issuer
},
'operationName': l10n.s_nfc_dialog_oath_rename_account,
'operationProcessing': l10n.s_nfc_dialog_oath_rename_account_processing,
'operationSuccess': l10n.s_nfc_dialog_oath_rename_account_success,
'operationFailure': l10n.s_nfc_dialog_oath_rename_account_failure,
});
}

View File

@ -15,113 +15,132 @@
*/ */
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
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 '../app/models.dart';
import '../app/state.dart'; import '../app/state.dart';
import '../app/views/user_interaction.dart'; import '../widgets/pulsing.dart';
import 'views/nfc/nfc_activity_widget.dart'; import 'state.dart';
import 'views/nfc/nfc_activity_overlay.dart';
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog'); const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
// _DDesc contains id of title resource for the dialog final androidDialogProvider =
enum _DTitle { NotifierProvider<_DialogProvider, int>(_DialogProvider.new);
tapKey,
operationSuccessful,
operationFailed,
invalid;
static _DTitle fromId(int? id) => class _DialogProvider extends Notifier<int> {
const { Timer? processingTimer;
0: _DTitle.tapKey, bool explicitAction = false;
1: _DTitle.operationSuccessful,
2: _DTitle.operationFailed
}[id] ??
_DTitle.invalid;
}
// _DDesc contains action description in the dialog @override
enum _DDesc { int build() {
// oath descriptions final l10n = ref.read(l10nProvider);
oathResetApplet, ref.listen(androidNfcActivityProvider, (previous, current) {
oathUnlockSession, final notifier = ref.read(nfcActivityCommandNotifier.notifier);
oathSetPassword,
oathUnsetPassword,
oathAddAccount,
oathRenameAccount,
oathDeleteAccount,
oathCalculateCode,
oathActionFailure,
oathAddMultipleAccounts,
// FIDO descriptions
fidoResetApplet,
fidoUnlockSession,
fidoSetPin,
fidoDeleteCredential,
fidoDeleteFingerprint,
fidoRenameFingerprint,
fidoRegisterFingerprint,
fidoEnableEnterpriseAttestation,
fidoActionFailure,
// Others
invalid;
static const int dialogDescriptionOathIndex = 100; if (!explicitAction) {
static const int dialogDescriptionFidoIndex = 200; // setup properties for ad-hoc action
ref.read(nfcActivityWidgetNotifier.notifier).setDialogProperties(
operationProcessing: l10n.s_nfc_dialog_read_key,
operationFailure: l10n.s_nfc_dialog_read_key_failure,
showSuccess: false,
);
}
static _DDesc fromId(int? id) => final properties = ref.read(nfcActivityWidgetNotifier);
const {
dialogDescriptionOathIndex + 0: oathResetApplet,
dialogDescriptionOathIndex + 1: oathUnlockSession,
dialogDescriptionOathIndex + 2: oathSetPassword,
dialogDescriptionOathIndex + 3: oathUnsetPassword,
dialogDescriptionOathIndex + 4: oathAddAccount,
dialogDescriptionOathIndex + 5: oathRenameAccount,
dialogDescriptionOathIndex + 6: oathDeleteAccount,
dialogDescriptionOathIndex + 7: oathCalculateCode,
dialogDescriptionOathIndex + 8: oathActionFailure,
dialogDescriptionOathIndex + 9: oathAddMultipleAccounts,
dialogDescriptionFidoIndex + 0: fidoResetApplet,
dialogDescriptionFidoIndex + 1: fidoUnlockSession,
dialogDescriptionFidoIndex + 2: fidoSetPin,
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint,
dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation,
dialogDescriptionFidoIndex + 8: fidoActionFailure,
}[id] ??
_DDesc.invalid;
}
final androidDialogProvider = Provider<_DialogProvider>( debugPrint('XXX now it is: $current');
(ref) { switch (current) {
return _DialogProvider(ref.watch(withContextProvider)); case NfcActivity.processingStarted:
}, processingTimer?.cancel();
);
class _DialogProvider { debugPrint('XXX explicit action: $explicitAction');
final WithContext _withContext; final timeout = explicitAction ? 300 : 200;
final Widget _icon = const NfcActivityWidget(width: 64, height: 64);
UserInteractionController? _controller; processingTimer = Timer(Duration(milliseconds: timeout), () {
if (!explicitAction) {
// show the widget
notifier.update(NfcActivityWidgetCommand(
action: NfcActivityWidgetActionShowWidget(
child: _NfcActivityWidgetView(
title: properties.operationProcessing,
subtitle: '',
inProgress: true,
))));
} else {
// the processing view will only be shown if the timer is still active
notifier.update(NfcActivityWidgetCommand(
action: NfcActivityWidgetActionSetWidgetData(
child: _NfcActivityWidgetView(
title: properties.operationProcessing,
subtitle: l10n.s_nfc_dialog_hold_key,
inProgress: true,
))));
}
});
break;
case NfcActivity.processingFinished:
explicitAction = false; // next action might not be explicit
processingTimer?.cancel();
if (properties.showSuccess ?? false) {
notifier.update(NfcActivityWidgetCommand(
action: NfcActivityWidgetActionSetWidgetData(
child: NfcActivityClosingCountdownWidgetView(
closeInSec: 5,
child: _NfcActivityWidgetView(
title: properties.operationSuccess,
subtitle: l10n.s_nfc_dialog_remove_key,
inProgress: false,
),
))));
} else {
// directly hide
notifier.update(NfcActivityWidgetCommand(
action: const NfcActivityWidgetActionHideWidget(timeoutMs: 0)));
}
break;
case NfcActivity.processingInterrupted:
explicitAction = false; // next action might not be explicit
notifier.update(NfcActivityWidgetCommand(
action: NfcActivityWidgetActionSetWidgetData(
child: _NfcActivityWidgetView(
title: properties.operationFailure,
inProgress: false,
))));
break;
case NfcActivity.notActive:
debugPrint('Received not handled notActive');
break;
case NfcActivity.ready:
debugPrint('Received not handled ready');
}
});
_DialogProvider(this._withContext) {
_channel.setMethodCallHandler((call) async { _channel.setMethodCallHandler((call) async {
final args = jsonDecode(call.arguments); final notifier = ref.read(nfcActivityCommandNotifier.notifier);
final properties = ref.read(nfcActivityWidgetNotifier);
switch (call.method) { switch (call.method) {
case 'close':
_closeDialog();
break;
case 'show': case 'show':
await _showDialog(args['title'], args['description']); explicitAction = true;
notifier.update(NfcActivityWidgetCommand(
action: NfcActivityWidgetActionShowWidget(
child: _NfcActivityWidgetView(
title: l10n.s_nfc_dialog_tap_for(
properties.operationName ?? '[OPERATION NAME MISSING]'),
subtitle: '',
inProgress: false,
))));
break; break;
case 'state':
await _updateDialogState(args['title'], args['description']); case 'close':
notifier.update(NfcActivityWidgetCommand(
action: const NfcActivityWidgetActionHideWidget(timeoutMs: 0)));
break; break;
default: default:
throw PlatformException( throw PlatformException(
code: 'NotImplemented', code: 'NotImplemented',
@ -129,71 +148,112 @@ class _DialogProvider {
); );
} }
}); });
return 0;
} }
void _closeDialog() { void cancelDialog() async {
_controller?.close(); debugPrint('Cancelled dialog');
_controller = null; explicitAction = false;
await _channel.invokeMethod('cancel');
} }
String _getTitle(BuildContext context, int? titleId) { Future<void> waitForDialogClosed() async {
final l10n = AppLocalizations.of(context)!; final completer = Completer();
return switch (_DTitle.fromId(titleId)) {
_DTitle.tapKey => l10n.l_nfc_dialog_tap_key,
_DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success,
_DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed,
_ => ''
};
}
String _getDialogDescription(BuildContext context, int? descriptionId) { Timer.periodic(
final l10n = AppLocalizations.of(context)!; const Duration(milliseconds: 200),
return switch (_DDesc.fromId(descriptionId)) { (timer) {
_DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset, if (!ref.read(nfcActivityWidgetNotifier.select((s) => s.isShowing))) {
_DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock, timer.cancel();
_DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password, completer.complete();
_DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password, }
_DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account, },
_DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account, );
_DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account,
_DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code,
_DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure,
_DDesc.oathAddMultipleAccounts =>
l10n.s_nfc_dialog_oath_add_multiple_accounts,
_DDesc.fidoResetApplet => l10n.s_nfc_dialog_fido_reset,
_DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock,
_DDesc.fidoSetPin => l10n.l_nfc_dialog_fido_set_pin,
_DDesc.fidoDeleteCredential => l10n.s_nfc_dialog_fido_delete_credential,
_DDesc.fidoDeleteFingerprint => l10n.s_nfc_dialog_fido_delete_fingerprint,
_DDesc.fidoRenameFingerprint => l10n.s_nfc_dialog_fido_rename_fingerprint,
_DDesc.fidoActionFailure => l10n.s_nfc_dialog_fido_failure,
_ => ''
};
}
Future<void> _updateDialogState(int? title, int? description) async { await completer.future;
await _withContext((context) async { }
_controller?.updateContent( }
title: _getTitle(context, title),
description: _getDialogDescription(context, description), class _NfcActivityWidgetView extends StatelessWidget {
icon: (_DDesc.fromId(description) != _DDesc.oathActionFailure) final bool inProgress;
? _icon final String? title;
: const Icon(Icons.warning_amber_rounded, size: 64), final String? subtitle;
);
}); const _NfcActivityWidgetView(
} {required this.title, this.subtitle, this.inProgress = false});
Future<void> _showDialog(int title, int description) async { @override
_controller = await _withContext((context) async { Widget build(BuildContext context) {
return promptUserInteraction( return Padding(
context, padding: const EdgeInsets.symmetric(horizontal: 8.0),
title: _getTitle(context, title), child: Column(
description: _getDialogDescription(context, description), children: [
icon: _icon, Text(title ?? 'Missing title',
onCancel: () { textAlign: TextAlign.center,
_channel.invokeMethod('cancel'); style: Theme.of(context).textTheme.titleLarge),
}, const SizedBox(height: 8),
); if (subtitle != null)
}); Text(subtitle!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 32),
inProgress
? const Pulsing(child: Icon(Symbols.contactless, size: 64))
: const Icon(Symbols.contactless, size: 64),
const SizedBox(height: 24)
],
),
);
}
}
class MethodChannelHelper {
final ProviderRef _ref;
final MethodChannel _channel;
const MethodChannelHelper(this._ref, this._channel);
Future<dynamic> invoke(String method,
{String? operationName,
String? operationSuccess,
String? operationProcessing,
String? operationFailure,
bool? showSuccess,
Map<String, dynamic> arguments = const {}}) async {
final notifier = _ref.read(nfcActivityWidgetNotifier.notifier);
notifier.setDialogProperties(
operationName: operationName,
operationProcessing: operationProcessing,
operationSuccess: operationSuccess,
operationFailure: operationFailure,
showSuccess: showSuccess);
final result = await _channel.invokeMethod(method, arguments);
await _ref.read(androidDialogProvider.notifier).waitForDialogClosed();
return result;
}
}
class MethodChannelNotifier extends Notifier<void> {
final MethodChannel _channel;
MethodChannelNotifier(this._channel);
@override
void build() {}
Future<dynamic> invoke(String name,
[Map<String, dynamic> params = const {}]) async {
final notifier = ref.read(nfcActivityWidgetNotifier.notifier);
notifier.setDialogProperties(
operationName: params['operationName'],
operationProcessing: params['operationProcessing'],
operationSuccess: params['operationSuccess'],
operationFailure: params['operationFailure'],
showSuccess: params['showSuccess']);
final result = await _channel.invokeMethod(name, params['callArgs']);
await ref.read(androidDialogProvider.notifier).waitForDialogClosed();
return result;
} }
} }

View File

@ -0,0 +1,84 @@
/*
* 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.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/models.dart';
import '../../tap_request_dialog.dart';
import 'nfc_activity_overlay.dart';
final nfcActivityCommandListener = Provider<_NfcActivityCommandListener>(
(ref) => _NfcActivityCommandListener(ref));
class _NfcActivityCommandListener {
final ProviderRef _ref;
ProviderSubscription<NfcActivityWidgetAction>? listener;
_NfcActivityCommandListener(this._ref);
void startListener(BuildContext context) {
debugPrint('XXX Started listener');
listener?.close();
listener = _ref.listen(nfcActivityCommandNotifier.select((c) => c.action),
(previous, action) {
debugPrint(
'XXX Change in command for Overlay: $previous -> $action in context: $context');
switch (action) {
case (NfcActivityWidgetActionShowWidget a):
_show(context, a.child);
break;
case (NfcActivityWidgetActionSetWidgetData a):
_ref.read(nfcActivityWidgetNotifier.notifier).update(a.child);
break;
case (NfcActivityWidgetActionHideWidget _):
_hide(context);
break;
case (NfcActivityWidgetActionCancelWidget _):
_ref.read(androidDialogProvider.notifier).cancelDialog();
_hide(context);
break;
}
});
}
void _show(BuildContext context, Widget child) async {
final widgetNotifier = _ref.read(nfcActivityWidgetNotifier.notifier);
widgetNotifier.update(child);
if (!_ref.read(nfcActivityWidgetNotifier.select((s) => s.isShowing))) {
widgetNotifier.setShowing(true);
final result = await showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return const NfcBottomSheet();
});
debugPrint('XXX result is: $result');
if (result == null) {
// the modal sheet was cancelled by Back button, close button or dismiss
_ref.read(androidDialogProvider.notifier).cancelDialog();
}
widgetNotifier.setShowing(false);
}
}
void _hide(BuildContext context) {
if (_ref.read(nfcActivityWidgetNotifier.select((s) => s.isShowing))) {
Navigator.of(context).pop('AFTER OP');
_ref.read(nfcActivityWidgetNotifier.notifier).setShowing(false);
}
}
}

View File

@ -0,0 +1,172 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../app/models.dart';
import '../../state.dart';
final nfcActivityCommandNotifier = NotifierProvider<
_NfcActivityWidgetCommandNotifier,
NfcActivityWidgetCommand>(_NfcActivityWidgetCommandNotifier.new);
class _NfcActivityWidgetCommandNotifier
extends Notifier<NfcActivityWidgetCommand> {
@override
NfcActivityWidgetCommand build() {
return NfcActivityWidgetCommand(action: const NfcActivityWidgetAction());
}
void update(NfcActivityWidgetCommand command) {
state = command;
}
}
final nfcActivityWidgetNotifier =
NotifierProvider<_NfcActivityWidgetNotifier, NfcActivityWidgetState>(
_NfcActivityWidgetNotifier.new);
class NfcActivityClosingCountdownWidgetView extends ConsumerStatefulWidget {
final int closeInSec;
final Widget child;
const NfcActivityClosingCountdownWidgetView(
{super.key, required this.child, this.closeInSec = 3});
@override
ConsumerState<NfcActivityClosingCountdownWidgetView> createState() =>
_NfcActivityClosingCountdownWidgetViewState();
}
class _NfcActivityClosingCountdownWidgetViewState
extends ConsumerState<NfcActivityClosingCountdownWidgetView> {
late int counter;
late Timer? timer;
bool shouldHide = false;
@override
Widget build(BuildContext context) {
ref.listen(androidNfcActivityProvider, (previous, current) {
if (current == NfcActivity.ready) {
timer?.cancel();
hideNow();
}
});
return Stack(
fit: StackFit.loose,
children: [
Center(child: widget.child),
Positioned(
bottom: 0,
right: 0,
child: counter > 0
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Closing in $counter'),
)
: const SizedBox(),
)
],
);
}
@override
void initState() {
super.initState();
counter = widget.closeInSec;
timer = Timer(const Duration(seconds: 1), onTimer);
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
void onTimer() async {
timer?.cancel();
setState(() {
counter--;
});
if (counter > 0) {
timer = Timer(const Duration(seconds: 1), onTimer);
} else {
hideNow();
}
}
void hideNow() {
debugPrint('XXX closing because have to!');
ref.read(nfcActivityCommandNotifier.notifier).update(
NfcActivityWidgetCommand(
action: const NfcActivityWidgetActionHideWidget(timeoutMs: 0)));
}
}
class _NfcActivityWidgetNotifier extends Notifier<NfcActivityWidgetState> {
@override
NfcActivityWidgetState build() {
return NfcActivityWidgetState(isShowing: false, child: const SizedBox());
}
void update(Widget child) {
state = state.copyWith(child: child);
}
void setShowing(bool value) {
state = state.copyWith(isShowing: value);
}
void setDialogProperties(
{String? operationName,
String? operationProcessing,
String? operationSuccess,
String? operationFailure,
bool? showSuccess}) {
state = state.copyWith(
operationName: operationName,
operationProcessing: operationProcessing,
operationSuccess: operationSuccess,
operationFailure: operationFailure,
showSuccess: showSuccess);
}
}
class NfcBottomSheet extends ConsumerWidget {
const NfcBottomSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final widget = ref.watch(nfcActivityWidgetNotifier.select((s) => s.child));
final showCloseButton = ref.watch(
nfcActivityWidgetNotifier.select((s) => s.showCloseButton ?? false));
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showCloseButton) const SizedBox(height: 8),
if (showCloseButton)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Symbols.close, fill: 1, size: 24))
],
),
),
if (showCloseButton) const SizedBox(height: 16),
if (!showCloseButton) const SizedBox(height: 48),
widget,
const SizedBox(height: 32),
],
);
}
}

View File

@ -170,3 +170,46 @@ class _ColorConverter implements JsonConverter<Color?, int?> {
@override @override
int? toJson(Color? object) => object?.value; int? toJson(Color? object) => object?.value;
} }
class NfcActivityWidgetAction {
const NfcActivityWidgetAction();
}
class NfcActivityWidgetActionShowWidget extends NfcActivityWidgetAction {
final Widget child;
const NfcActivityWidgetActionShowWidget({required this.child});
}
class NfcActivityWidgetActionHideWidget extends NfcActivityWidgetAction {
final int timeoutMs;
const NfcActivityWidgetActionHideWidget({required this.timeoutMs});
}
class NfcActivityWidgetActionCancelWidget extends NfcActivityWidgetAction {
const NfcActivityWidgetActionCancelWidget();
}
class NfcActivityWidgetActionSetWidgetData extends NfcActivityWidgetAction {
final Widget child;
const NfcActivityWidgetActionSetWidgetData({required this.child});
}
@freezed
class NfcActivityWidgetState with _$NfcActivityWidgetState {
factory NfcActivityWidgetState(
{required bool isShowing,
required Widget child,
bool? showCloseButton,
bool? showSuccess,
String? operationName,
String? operationProcessing,
String? operationSuccess,
String? operationFailure}) = _NfcActivityWidgetState;
}
@freezed
class NfcActivityWidgetCommand with _$NfcActivityWidgetCommand {
factory NfcActivityWidgetCommand({
@Default(NfcActivityWidgetAction()) NfcActivityWidgetAction action,
}) = _NfcActivityWidgetCommand;
}

View File

@ -1346,3 +1346,410 @@ abstract class _KeyCustomization implements KeyCustomization {
_$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith => _$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
/// @nodoc
mixin _$NfcActivityWidgetState {
bool get isShowing => throw _privateConstructorUsedError;
Widget get child => throw _privateConstructorUsedError;
bool? get showCloseButton => throw _privateConstructorUsedError;
bool? get showSuccess => throw _privateConstructorUsedError;
String? get operationName => throw _privateConstructorUsedError;
String? get operationProcessing => throw _privateConstructorUsedError;
String? get operationSuccess => throw _privateConstructorUsedError;
String? get operationFailure => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$NfcActivityWidgetStateCopyWith<NfcActivityWidgetState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NfcActivityWidgetStateCopyWith<$Res> {
factory $NfcActivityWidgetStateCopyWith(NfcActivityWidgetState value,
$Res Function(NfcActivityWidgetState) then) =
_$NfcActivityWidgetStateCopyWithImpl<$Res, NfcActivityWidgetState>;
@useResult
$Res call(
{bool isShowing,
Widget child,
bool? showCloseButton,
bool? showSuccess,
String? operationName,
String? operationProcessing,
String? operationSuccess,
String? operationFailure});
}
/// @nodoc
class _$NfcActivityWidgetStateCopyWithImpl<$Res,
$Val extends NfcActivityWidgetState>
implements $NfcActivityWidgetStateCopyWith<$Res> {
_$NfcActivityWidgetStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isShowing = null,
Object? child = null,
Object? showCloseButton = freezed,
Object? showSuccess = freezed,
Object? operationName = freezed,
Object? operationProcessing = freezed,
Object? operationSuccess = freezed,
Object? operationFailure = freezed,
}) {
return _then(_value.copyWith(
isShowing: null == isShowing
? _value.isShowing
: isShowing // ignore: cast_nullable_to_non_nullable
as bool,
child: null == child
? _value.child
: child // ignore: cast_nullable_to_non_nullable
as Widget,
showCloseButton: freezed == showCloseButton
? _value.showCloseButton
: showCloseButton // ignore: cast_nullable_to_non_nullable
as bool?,
showSuccess: freezed == showSuccess
? _value.showSuccess
: showSuccess // ignore: cast_nullable_to_non_nullable
as bool?,
operationName: freezed == operationName
? _value.operationName
: operationName // ignore: cast_nullable_to_non_nullable
as String?,
operationProcessing: freezed == operationProcessing
? _value.operationProcessing
: operationProcessing // ignore: cast_nullable_to_non_nullable
as String?,
operationSuccess: freezed == operationSuccess
? _value.operationSuccess
: operationSuccess // ignore: cast_nullable_to_non_nullable
as String?,
operationFailure: freezed == operationFailure
? _value.operationFailure
: operationFailure // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$NfcActivityWidgetStateImplCopyWith<$Res>
implements $NfcActivityWidgetStateCopyWith<$Res> {
factory _$$NfcActivityWidgetStateImplCopyWith(
_$NfcActivityWidgetStateImpl value,
$Res Function(_$NfcActivityWidgetStateImpl) then) =
__$$NfcActivityWidgetStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool isShowing,
Widget child,
bool? showCloseButton,
bool? showSuccess,
String? operationName,
String? operationProcessing,
String? operationSuccess,
String? operationFailure});
}
/// @nodoc
class __$$NfcActivityWidgetStateImplCopyWithImpl<$Res>
extends _$NfcActivityWidgetStateCopyWithImpl<$Res,
_$NfcActivityWidgetStateImpl>
implements _$$NfcActivityWidgetStateImplCopyWith<$Res> {
__$$NfcActivityWidgetStateImplCopyWithImpl(
_$NfcActivityWidgetStateImpl _value,
$Res Function(_$NfcActivityWidgetStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isShowing = null,
Object? child = null,
Object? showCloseButton = freezed,
Object? showSuccess = freezed,
Object? operationName = freezed,
Object? operationProcessing = freezed,
Object? operationSuccess = freezed,
Object? operationFailure = freezed,
}) {
return _then(_$NfcActivityWidgetStateImpl(
isShowing: null == isShowing
? _value.isShowing
: isShowing // ignore: cast_nullable_to_non_nullable
as bool,
child: null == child
? _value.child
: child // ignore: cast_nullable_to_non_nullable
as Widget,
showCloseButton: freezed == showCloseButton
? _value.showCloseButton
: showCloseButton // ignore: cast_nullable_to_non_nullable
as bool?,
showSuccess: freezed == showSuccess
? _value.showSuccess
: showSuccess // ignore: cast_nullable_to_non_nullable
as bool?,
operationName: freezed == operationName
? _value.operationName
: operationName // ignore: cast_nullable_to_non_nullable
as String?,
operationProcessing: freezed == operationProcessing
? _value.operationProcessing
: operationProcessing // ignore: cast_nullable_to_non_nullable
as String?,
operationSuccess: freezed == operationSuccess
? _value.operationSuccess
: operationSuccess // ignore: cast_nullable_to_non_nullable
as String?,
operationFailure: freezed == operationFailure
? _value.operationFailure
: operationFailure // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$NfcActivityWidgetStateImpl implements _NfcActivityWidgetState {
_$NfcActivityWidgetStateImpl(
{required this.isShowing,
required this.child,
this.showCloseButton,
this.showSuccess,
this.operationName,
this.operationProcessing,
this.operationSuccess,
this.operationFailure});
@override
final bool isShowing;
@override
final Widget child;
@override
final bool? showCloseButton;
@override
final bool? showSuccess;
@override
final String? operationName;
@override
final String? operationProcessing;
@override
final String? operationSuccess;
@override
final String? operationFailure;
@override
String toString() {
return 'NfcActivityWidgetState(isShowing: $isShowing, child: $child, showCloseButton: $showCloseButton, showSuccess: $showSuccess, operationName: $operationName, operationProcessing: $operationProcessing, operationSuccess: $operationSuccess, operationFailure: $operationFailure)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NfcActivityWidgetStateImpl &&
(identical(other.isShowing, isShowing) ||
other.isShowing == isShowing) &&
(identical(other.child, child) || other.child == child) &&
(identical(other.showCloseButton, showCloseButton) ||
other.showCloseButton == showCloseButton) &&
(identical(other.showSuccess, showSuccess) ||
other.showSuccess == showSuccess) &&
(identical(other.operationName, operationName) ||
other.operationName == operationName) &&
(identical(other.operationProcessing, operationProcessing) ||
other.operationProcessing == operationProcessing) &&
(identical(other.operationSuccess, operationSuccess) ||
other.operationSuccess == operationSuccess) &&
(identical(other.operationFailure, operationFailure) ||
other.operationFailure == operationFailure));
}
@override
int get hashCode => Object.hash(
runtimeType,
isShowing,
child,
showCloseButton,
showSuccess,
operationName,
operationProcessing,
operationSuccess,
operationFailure);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$NfcActivityWidgetStateImplCopyWith<_$NfcActivityWidgetStateImpl>
get copyWith => __$$NfcActivityWidgetStateImplCopyWithImpl<
_$NfcActivityWidgetStateImpl>(this, _$identity);
}
abstract class _NfcActivityWidgetState implements NfcActivityWidgetState {
factory _NfcActivityWidgetState(
{required final bool isShowing,
required final Widget child,
final bool? showCloseButton,
final bool? showSuccess,
final String? operationName,
final String? operationProcessing,
final String? operationSuccess,
final String? operationFailure}) = _$NfcActivityWidgetStateImpl;
@override
bool get isShowing;
@override
Widget get child;
@override
bool? get showCloseButton;
@override
bool? get showSuccess;
@override
String? get operationName;
@override
String? get operationProcessing;
@override
String? get operationSuccess;
@override
String? get operationFailure;
@override
@JsonKey(ignore: true)
_$$NfcActivityWidgetStateImplCopyWith<_$NfcActivityWidgetStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$NfcActivityWidgetCommand {
NfcActivityWidgetAction get action => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$NfcActivityWidgetCommandCopyWith<NfcActivityWidgetCommand> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NfcActivityWidgetCommandCopyWith<$Res> {
factory $NfcActivityWidgetCommandCopyWith(NfcActivityWidgetCommand value,
$Res Function(NfcActivityWidgetCommand) then) =
_$NfcActivityWidgetCommandCopyWithImpl<$Res, NfcActivityWidgetCommand>;
@useResult
$Res call({NfcActivityWidgetAction action});
}
/// @nodoc
class _$NfcActivityWidgetCommandCopyWithImpl<$Res,
$Val extends NfcActivityWidgetCommand>
implements $NfcActivityWidgetCommandCopyWith<$Res> {
_$NfcActivityWidgetCommandCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? action = null,
}) {
return _then(_value.copyWith(
action: null == action
? _value.action
: action // ignore: cast_nullable_to_non_nullable
as NfcActivityWidgetAction,
) as $Val);
}
}
/// @nodoc
abstract class _$$NfcActivityWidgetCommandImplCopyWith<$Res>
implements $NfcActivityWidgetCommandCopyWith<$Res> {
factory _$$NfcActivityWidgetCommandImplCopyWith(
_$NfcActivityWidgetCommandImpl value,
$Res Function(_$NfcActivityWidgetCommandImpl) then) =
__$$NfcActivityWidgetCommandImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({NfcActivityWidgetAction action});
}
/// @nodoc
class __$$NfcActivityWidgetCommandImplCopyWithImpl<$Res>
extends _$NfcActivityWidgetCommandCopyWithImpl<$Res,
_$NfcActivityWidgetCommandImpl>
implements _$$NfcActivityWidgetCommandImplCopyWith<$Res> {
__$$NfcActivityWidgetCommandImplCopyWithImpl(
_$NfcActivityWidgetCommandImpl _value,
$Res Function(_$NfcActivityWidgetCommandImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? action = null,
}) {
return _then(_$NfcActivityWidgetCommandImpl(
action: null == action
? _value.action
: action // ignore: cast_nullable_to_non_nullable
as NfcActivityWidgetAction,
));
}
}
/// @nodoc
class _$NfcActivityWidgetCommandImpl implements _NfcActivityWidgetCommand {
_$NfcActivityWidgetCommandImpl(
{this.action = const NfcActivityWidgetAction()});
@override
@JsonKey()
final NfcActivityWidgetAction action;
@override
String toString() {
return 'NfcActivityWidgetCommand(action: $action)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NfcActivityWidgetCommandImpl &&
(identical(other.action, action) || other.action == action));
}
@override
int get hashCode => Object.hash(runtimeType, action);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$NfcActivityWidgetCommandImplCopyWith<_$NfcActivityWidgetCommandImpl>
get copyWith => __$$NfcActivityWidgetCommandImplCopyWithImpl<
_$NfcActivityWidgetCommandImpl>(this, _$identity);
}
abstract class _NfcActivityWidgetCommand implements NfcActivityWidgetCommand {
factory _NfcActivityWidgetCommand({final NfcActivityWidgetAction action}) =
_$NfcActivityWidgetCommandImpl;
@override
NfcActivityWidgetAction get action;
@override
@JsonKey(ignore: true)
_$$NfcActivityWidgetCommandImplCopyWith<_$NfcActivityWidgetCommandImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -885,28 +885,95 @@
"l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen", "l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen",
"s_allow_screenshots": "Bildschirmfotos erlauben", "s_allow_screenshots": "Bildschirmfotos erlauben",
"l_nfc_dialog_tap_key": "Halten Sie Ihren Schlüssel dagegen", "@_ndef_oath_actions": {},
"s_nfc_dialog_operation_success": "Erfolgreich", "s_nfc_dialog_oath_reset": null,
"s_nfc_dialog_operation_failed": "Fehlgeschlagen", "s_nfc_dialog_oath_reset_processing": null,
"s_nfc_dialog_oath_reset_success": null,
"s_nfc_dialog_oath_reset_failure": null,
"s_nfc_dialog_oath_reset": "Aktion: OATH-Anwendung zurücksetzen", "s_nfc_dialog_oath_unlock": null,
"s_nfc_dialog_oath_unlock": "Aktion: OATH-Anwendung entsperren", "s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_set_password": "Aktion: OATH-Passwort setzen", "s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unset_password": "Aktion: OATH-Passwort entfernen", "s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_oath_add_account": "Aktion: neues Konto hinzufügen",
"s_nfc_dialog_oath_rename_account": "Aktion: Konto umbenennen",
"s_nfc_dialog_oath_delete_account": "Aktion: Konto löschen",
"s_nfc_dialog_oath_calculate_code": "Aktion: OATH-Code berechnen",
"s_nfc_dialog_oath_failure": "OATH-Operation fehlgeschlagen",
"s_nfc_dialog_oath_add_multiple_accounts": "Aktion: mehrere Konten hinzufügen",
"s_nfc_dialog_fido_reset": "Aktion: FIDO-Anwendung zurücksetzen", "s_nfc_dialog_oath_set_password": null,
"s_nfc_dialog_fido_unlock": "Aktion: FIDO-Anwendung entsperren", "s_nfc_dialog_oath_change_password": null,
"l_nfc_dialog_fido_set_pin": "Aktion: FIDO-PIN setzen oder ändern", "s_nfc_dialog_oath_set_password_processing": null,
"s_nfc_dialog_fido_delete_credential": "Aktion: Passkey löschen", "s_nfc_dialog_oath_change_password_processing": null,
"s_nfc_dialog_fido_delete_fingerprint": "Aktion: Fingerabdruck löschen", "s_nfc_dialog_oath_set_password_success": null,
"s_nfc_dialog_fido_rename_fingerprint": "Aktion: Fingerabdruck umbenennen", "s_nfc_dialog_oath_change_password_success": null,
"s_nfc_dialog_fido_failure": "FIDO-Operation fehlgeschlagen", "s_nfc_dialog_oath_set_password_failure": null,
"s_nfc_dialog_oath_change_password_failure": null,
"s_nfc_dialog_oath_remove_password": null,
"s_nfc_dialog_oath_remove_password_processing": null,
"s_nfc_dialog_oath_remove_password_success": null,
"s_nfc_dialog_oath_remove_password_failure": null,
"s_nfc_dialog_oath_add_account": null,
"s_nfc_dialog_oath_add_account_processing": null,
"s_nfc_dialog_oath_add_account_success": null,
"s_nfc_dialog_oath_add_account_failure": null,
"s_nfc_dialog_oath_rename_account": null,
"s_nfc_dialog_oath_rename_account_processing": null,
"s_nfc_dialog_oath_rename_account_success": null,
"s_nfc_dialog_oath_rename_account_failure": null,
"s_nfc_dialog_oath_delete_account": null,
"s_nfc_dialog_oath_delete_account_processing": null,
"s_nfc_dialog_oath_delete_account_success": null,
"s_nfc_dialog_oath_delete_account_failure": null,
"s_nfc_dialog_oath_calculate_code": null,
"s_nfc_dialog_oath_calculate_code_processing": null,
"s_nfc_dialog_oath_calculate_code_success": null,
"s_nfc_dialog_oath_calculate_code_failure": null,
"s_nfc_dialog_oath_add_multiple_accounts": null,
"s_nfc_dialog_oath_add_multiple_accounts_processing": null,
"s_nfc_dialog_oath_add_multiple_accounts_success": null,
"s_nfc_dialog_oath_add_multiple_accounts_failure": null,
"@_ndef_fido_actions": {},
"s_nfc_dialog_fido_reset": null,
"s_nfc_dialog_fido_reset_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": null,
"s_nfc_dialog_fido_unlock": null,
"s_nfc_dialog_fido_unlock_processing": null,
"s_nfc_dialog_fido_unlock_success": null,
"s_nfc_dialog_fido_unlock_failure": null,
"s_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_set_pin_processing": null,
"s_nfc_dialog_fido_set_pin_success": null,
"s_nfc_dialog_fido_set_pin_failure": null,
"s_nfc_dialog_fido_change_pin": null,
"s_nfc_dialog_fido_change_pin_processing": null,
"s_nfc_dialog_fido_change_pin_success": null,
"s_nfc_dialog_fido_change_pin_failure": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_credential_processing": null,
"s_nfc_dialog_fido_delete_credential_success": null,
"s_nfc_dialog_fido_delete_credential_failure": null,
"@_ndef_operations": {},
"s_nfc_dialog_tap_for": null,
"@s_nfc_dialog_tap_for": {
"placeholders": {
"operation": {}
}
},
"s_nfc_dialog_read_key": null,
"s_nfc_dialog_read_key_failure": null,
"s_nfc_dialog_hold_key": null,
"s_nfc_dialog_remove_key": null,
"@_ndef": {}, "@_ndef": {},
"p_ndef_set_otp": "OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.", "p_ndef_set_otp": "OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.",

View File

@ -885,28 +885,95 @@
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB", "l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
"s_allow_screenshots": "Allow screenshots", "s_allow_screenshots": "Allow screenshots",
"l_nfc_dialog_tap_key": "Tap and hold your key", "@_ndef_oath_actions": {},
"s_nfc_dialog_operation_success": "Success", "s_nfc_dialog_oath_reset": "reset Accounts",
"s_nfc_dialog_operation_failed": "Failed", "s_nfc_dialog_oath_reset_processing": "Reset in progress",
"s_nfc_dialog_oath_reset_success": "Accounts reset",
"s_nfc_dialog_oath_reset_failure": "Failed to reset accounts",
"s_nfc_dialog_oath_reset": "Action: reset OATH application", "s_nfc_dialog_oath_unlock": "unlock",
"s_nfc_dialog_oath_unlock": "Action: unlock OATH application", "s_nfc_dialog_oath_unlock_processing": "Unlocking",
"s_nfc_dialog_oath_set_password": "Action: set OATH password", "s_nfc_dialog_oath_unlock_success": "Accounts unlocked",
"s_nfc_dialog_oath_unset_password": "Action: remove OATH password", "s_nfc_dialog_oath_unlock_failure": "Failed to unlock",
"s_nfc_dialog_oath_add_account": "Action: add new account",
"s_nfc_dialog_oath_rename_account": "Action: rename account",
"s_nfc_dialog_oath_delete_account": "Action: delete account",
"s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code",
"s_nfc_dialog_oath_failure": "OATH operation failed",
"s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts",
"s_nfc_dialog_fido_reset": "Action: reset FIDO application", "s_nfc_dialog_oath_set_password": "set password",
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO application", "s_nfc_dialog_oath_change_password": "change password",
"l_nfc_dialog_fido_set_pin": "Action: set or change the FIDO PIN", "s_nfc_dialog_oath_set_password_processing": "Setting password",
"s_nfc_dialog_fido_delete_credential": "Action: delete Passkey", "s_nfc_dialog_oath_change_password_processing": "Changing password",
"s_nfc_dialog_fido_delete_fingerprint": "Action: delete fingerprint", "s_nfc_dialog_oath_set_password_success": "Password set",
"s_nfc_dialog_fido_rename_fingerprint": "Action: rename fingerprint", "s_nfc_dialog_oath_change_password_success": "Password changed",
"s_nfc_dialog_fido_failure": "FIDO operation failed", "s_nfc_dialog_oath_set_password_failure": "Failed to set password",
"s_nfc_dialog_oath_change_password_failure": "Failed to change password",
"s_nfc_dialog_oath_remove_password": "remove password",
"s_nfc_dialog_oath_remove_password_processing": "Removing password",
"s_nfc_dialog_oath_remove_password_success": "Password removed",
"s_nfc_dialog_oath_remove_password_failure": "Failed to remove password",
"s_nfc_dialog_oath_add_account": "add account",
"s_nfc_dialog_oath_add_account_processing": "Adding account",
"s_nfc_dialog_oath_add_account_success": "Account added",
"s_nfc_dialog_oath_add_account_failure": "Failed to add account",
"s_nfc_dialog_oath_rename_account": "rename account",
"s_nfc_dialog_oath_rename_account_processing": "Renaming account",
"s_nfc_dialog_oath_rename_account_success": "Account renamed",
"s_nfc_dialog_oath_rename_account_failure": "Failed to rename account",
"s_nfc_dialog_oath_delete_account": "delete account",
"s_nfc_dialog_oath_delete_account_processing": "Deleting account",
"s_nfc_dialog_oath_delete_account_success": "Account deleted",
"s_nfc_dialog_oath_delete_account_failure": "Failed to delete account",
"s_nfc_dialog_oath_calculate_code": "calculate code",
"s_nfc_dialog_oath_calculate_code_processing": "Calculating",
"s_nfc_dialog_oath_calculate_code_success": "Code calculated",
"s_nfc_dialog_oath_calculate_code_failure": "Failed to calculate code",
"s_nfc_dialog_oath_add_multiple_accounts": "add selected accounts",
"s_nfc_dialog_oath_add_multiple_accounts_processing": "Adding accounts",
"s_nfc_dialog_oath_add_multiple_accounts_success": "Accounts added",
"s_nfc_dialog_oath_add_multiple_accounts_failure": "Failed to add accounts",
"@_ndef_fido_actions": {},
"s_nfc_dialog_fido_reset": "reset FIDO application",
"s_nfc_dialog_fido_reset_processing": "Resetting FIDO",
"s_nfc_dialog_fido_reset_success": "FIDO reset",
"s_nfc_dialog_fido_reset_failure": "FIDO reset failed",
"s_nfc_dialog_fido_unlock": "unlock",
"s_nfc_dialog_fido_unlock_processing": "Unlocking",
"s_nfc_dialog_fido_unlock_success": "unlocked",
"s_nfc_dialog_fido_unlock_failure": "Failed to unlock",
"s_nfc_dialog_fido_set_pin": "set PIN",
"s_nfc_dialog_fido_set_pin_processing": "Setting PIN",
"s_nfc_dialog_fido_set_pin_success": "PIN set",
"s_nfc_dialog_fido_set_pin_failure": "Failure setting PIN",
"s_nfc_dialog_fido_change_pin": "change PIN",
"s_nfc_dialog_fido_change_pin_processing": "Changing PIN",
"s_nfc_dialog_fido_change_pin_success": "PIN changed",
"s_nfc_dialog_fido_change_pin_failure": "Failure changing PIN",
"s_nfc_dialog_fido_delete_credential": "delete passkey",
"s_nfc_dialog_fido_delete_credential_processing": "Deleting passkey",
"s_nfc_dialog_fido_delete_credential_success": "Passkey deleted",
"s_nfc_dialog_fido_delete_credential_failure": "Failed to delete passkey",
"@_ndef_operations": {},
"s_nfc_dialog_tap_for": "Tap YubiKey to {operation}",
"@s_nfc_dialog_tap_for": {
"placeholders": {
"operation": {}
}
},
"s_nfc_dialog_read_key": "Reading YubiKey",
"s_nfc_dialog_read_key_failure": "Failed to read YubiKey, try again",
"s_nfc_dialog_hold_key": "Hold YubiKey",
"s_nfc_dialog_remove_key": "You can remove YubiKey",
"@_ndef": {}, "@_ndef": {},
"p_ndef_set_otp": "Successfully copied OTP code from YubiKey to clipboard.", "p_ndef_set_otp": "Successfully copied OTP code from YubiKey to clipboard.",

View File

@ -885,28 +885,95 @@
"l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB", "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB",
"s_allow_screenshots": "Autoriser captures d'écran", "s_allow_screenshots": "Autoriser captures d'écran",
"l_nfc_dialog_tap_key": "Appuyez et maintenez votre clé", "@_ndef_oath_actions": {},
"s_nfc_dialog_operation_success": "Succès", "s_nfc_dialog_oath_reset": null,
"s_nfc_dialog_operation_failed": "Échec", "s_nfc_dialog_oath_reset_processing": null,
"s_nfc_dialog_oath_reset_success": null,
"s_nfc_dialog_oath_reset_failure": null,
"s_nfc_dialog_oath_reset": "Action\u00a0: réinitialiser applet OATH", "s_nfc_dialog_oath_unlock": null,
"s_nfc_dialog_oath_unlock": "Action\u00a0: débloquer applet OATH", "s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_set_password": "Action\u00a0: définir mot de passe OATH", "s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unset_password": "Action\u00a0: supprimer mot de passe OATH", "s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_oath_add_account": "Action\u00a0: ajouter nouveau compte",
"s_nfc_dialog_oath_rename_account": "Action\u00a0: renommer compte",
"s_nfc_dialog_oath_delete_account": "Action\u00a0: supprimer compte",
"s_nfc_dialog_oath_calculate_code": "Action\u00a0: calculer code OATH",
"s_nfc_dialog_oath_failure": "Opération OATH impossible",
"s_nfc_dialog_oath_add_multiple_accounts": "Action\u00a0: ajouter plusieurs comptes",
"s_nfc_dialog_fido_reset": "Action : réinitialiser l'application FIDO", "s_nfc_dialog_oath_set_password": null,
"s_nfc_dialog_fido_unlock": "Action : déverrouiller l'application FIDO", "s_nfc_dialog_oath_change_password": null,
"l_nfc_dialog_fido_set_pin": "Action : définir ou modifier le code PIN FIDO", "s_nfc_dialog_oath_set_password_processing": null,
"s_nfc_dialog_fido_delete_credential": "Action : supprimer le Passkey", "s_nfc_dialog_oath_change_password_processing": null,
"s_nfc_dialog_fido_delete_fingerprint": "Action : supprimer l'empreinte digitale", "s_nfc_dialog_oath_set_password_success": null,
"s_nfc_dialog_fido_rename_fingerprint": "Action : renommer l'empreinte digitale", "s_nfc_dialog_oath_change_password_success": null,
"s_nfc_dialog_fido_failure": "Échec de l'opération FIDO", "s_nfc_dialog_oath_set_password_failure": null,
"s_nfc_dialog_oath_change_password_failure": null,
"s_nfc_dialog_oath_remove_password": null,
"s_nfc_dialog_oath_remove_password_processing": null,
"s_nfc_dialog_oath_remove_password_success": null,
"s_nfc_dialog_oath_remove_password_failure": null,
"s_nfc_dialog_oath_add_account": null,
"s_nfc_dialog_oath_add_account_processing": null,
"s_nfc_dialog_oath_add_account_success": null,
"s_nfc_dialog_oath_add_account_failure": null,
"s_nfc_dialog_oath_rename_account": null,
"s_nfc_dialog_oath_rename_account_processing": null,
"s_nfc_dialog_oath_rename_account_success": null,
"s_nfc_dialog_oath_rename_account_failure": null,
"s_nfc_dialog_oath_delete_account": null,
"s_nfc_dialog_oath_delete_account_processing": null,
"s_nfc_dialog_oath_delete_account_success": null,
"s_nfc_dialog_oath_delete_account_failure": null,
"s_nfc_dialog_oath_calculate_code": null,
"s_nfc_dialog_oath_calculate_code_processing": null,
"s_nfc_dialog_oath_calculate_code_success": null,
"s_nfc_dialog_oath_calculate_code_failure": null,
"s_nfc_dialog_oath_add_multiple_accounts": null,
"s_nfc_dialog_oath_add_multiple_accounts_processing": null,
"s_nfc_dialog_oath_add_multiple_accounts_success": null,
"s_nfc_dialog_oath_add_multiple_accounts_failure": null,
"@_ndef_fido_actions": {},
"s_nfc_dialog_fido_reset": null,
"s_nfc_dialog_fido_reset_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": null,
"s_nfc_dialog_fido_unlock": null,
"s_nfc_dialog_fido_unlock_processing": null,
"s_nfc_dialog_fido_unlock_success": null,
"s_nfc_dialog_fido_unlock_failure": null,
"s_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_set_pin_processing": null,
"s_nfc_dialog_fido_set_pin_success": null,
"s_nfc_dialog_fido_set_pin_failure": null,
"s_nfc_dialog_fido_change_pin": null,
"s_nfc_dialog_fido_change_pin_processing": null,
"s_nfc_dialog_fido_change_pin_success": null,
"s_nfc_dialog_fido_change_pin_failure": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_credential_processing": null,
"s_nfc_dialog_fido_delete_credential_success": null,
"s_nfc_dialog_fido_delete_credential_failure": null,
"@_ndef_operations": {},
"s_nfc_dialog_tap_for": null,
"@s_nfc_dialog_tap_for": {
"placeholders": {
"operation": {}
}
},
"s_nfc_dialog_read_key": null,
"s_nfc_dialog_read_key_failure": null,
"s_nfc_dialog_hold_key": null,
"s_nfc_dialog_remove_key": null,
"@_ndef": {}, "@_ndef": {},
"p_ndef_set_otp": "Code OTP copié de la YubiKey dans le presse-papiers.", "p_ndef_set_otp": "Code OTP copié de la YubiKey dans le presse-papiers.",

View File

@ -885,28 +885,95 @@
"l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます", "l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます",
"s_allow_screenshots": "スクリーンショットを許可", "s_allow_screenshots": "スクリーンショットを許可",
"l_nfc_dialog_tap_key": "キーをタップして長押しします", "@_ndef_oath_actions": {},
"s_nfc_dialog_operation_success": "成功",
"s_nfc_dialog_operation_failed": "失敗",
"s_nfc_dialog_oath_reset": "アクションOATHアプレットをリセット", "s_nfc_dialog_oath_reset": "アクションOATHアプレットをリセット",
"s_nfc_dialog_oath_unlock": "アクションOATHアプレットをロック解除", "s_nfc_dialog_oath_reset_processing": null,
"s_nfc_dialog_oath_set_password": "アクションOATHパスワードを設定", "s_nfc_dialog_oath_reset_success": null,
"s_nfc_dialog_oath_unset_password": "アクションOATHパスワードを削除", "s_nfc_dialog_oath_reset_failure": null,
"s_nfc_dialog_oath_add_account": "アクション:新しいアカウントを追加",
"s_nfc_dialog_oath_rename_account": "アクション:アカウント名を変更",
"s_nfc_dialog_oath_delete_account": "アクション:アカウントを削除",
"s_nfc_dialog_oath_calculate_code": "アクションOATHコードを計算",
"s_nfc_dialog_oath_failure": "OATH操作が失敗しました",
"s_nfc_dialog_oath_add_multiple_accounts": "アクション:複数アカウントを追加",
"s_nfc_dialog_oath_unlock": "アクションOATHアプレットをロック解除",
"s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_oath_set_password": "アクションOATHパスワードを設定",
"s_nfc_dialog_oath_change_password": null,
"s_nfc_dialog_oath_set_password_processing": null,
"s_nfc_dialog_oath_change_password_processing": null,
"s_nfc_dialog_oath_set_password_success": null,
"s_nfc_dialog_oath_change_password_success": null,
"s_nfc_dialog_oath_set_password_failure": null,
"s_nfc_dialog_oath_change_password_failure": null,
"s_nfc_dialog_oath_remove_password": null,
"s_nfc_dialog_oath_remove_password_processing": null,
"s_nfc_dialog_oath_remove_password_success": null,
"s_nfc_dialog_oath_remove_password_failure": null,
"s_nfc_dialog_oath_add_account": "アクション:新しいアカウントを追加",
"s_nfc_dialog_oath_add_account_processing": null,
"s_nfc_dialog_oath_add_account_success": null,
"s_nfc_dialog_oath_add_account_failure": null,
"s_nfc_dialog_oath_rename_account": "アクション:アカウント名を変更",
"s_nfc_dialog_oath_rename_account_processing": null,
"s_nfc_dialog_oath_rename_account_success": null,
"s_nfc_dialog_oath_rename_account_failure": null,
"s_nfc_dialog_oath_delete_account": "アクション:アカウントを削除",
"s_nfc_dialog_oath_delete_account_processing": null,
"s_nfc_dialog_oath_delete_account_success": null,
"s_nfc_dialog_oath_delete_account_failure": null,
"s_nfc_dialog_oath_calculate_code": "アクションOATHコードを計算",
"s_nfc_dialog_oath_calculate_code_processing": null,
"s_nfc_dialog_oath_calculate_code_success": null,
"s_nfc_dialog_oath_calculate_code_failure": null,
"s_nfc_dialog_oath_add_multiple_accounts": "アクション:複数アカウントを追加",
"s_nfc_dialog_oath_add_multiple_accounts_processing": null,
"s_nfc_dialog_oath_add_multiple_accounts_success": null,
"s_nfc_dialog_oath_add_multiple_accounts_failure": null,
"@_ndef_fido_actions": {},
"s_nfc_dialog_fido_reset": "アクション: FIDOアプリケーションをリセット", "s_nfc_dialog_fido_reset": "アクション: FIDOアプリケーションをリセット",
"s_nfc_dialog_fido_reset_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": null,
"s_nfc_dialog_fido_unlock": "アクションFIDOアプリケーションのロックを解除する", "s_nfc_dialog_fido_unlock": "アクションFIDOアプリケーションのロックを解除する",
"l_nfc_dialog_fido_set_pin": "アクションFIDOのPINの設定または変更", "s_nfc_dialog_fido_unlock_processing": null,
"s_nfc_dialog_fido_unlock_success": null,
"s_nfc_dialog_fido_unlock_failure": null,
"s_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_set_pin_processing": null,
"s_nfc_dialog_fido_set_pin_success": null,
"s_nfc_dialog_fido_set_pin_failure": null,
"s_nfc_dialog_fido_change_pin": null,
"s_nfc_dialog_fido_change_pin_processing": null,
"s_nfc_dialog_fido_change_pin_success": null,
"s_nfc_dialog_fido_change_pin_failure": null,
"s_nfc_dialog_fido_delete_credential": "アクション: パスキーを削除", "s_nfc_dialog_fido_delete_credential": "アクション: パスキーを削除",
"s_nfc_dialog_fido_delete_fingerprint": "アクション: 指紋の削除", "s_nfc_dialog_fido_delete_credential_processing": null,
"s_nfc_dialog_fido_rename_fingerprint": "アクション: 指紋の名前を変更する", "s_nfc_dialog_fido_delete_credential_success": null,
"s_nfc_dialog_fido_failure": "FIDO操作に失敗しました", "s_nfc_dialog_fido_delete_credential_failure": null,
"@_ndef_operations": {},
"s_nfc_dialog_tap_for": null,
"@s_nfc_dialog_tap_for": {
"placeholders": {
"operation": {}
}
},
"s_nfc_dialog_read_key": null,
"s_nfc_dialog_read_key_failure": null,
"s_nfc_dialog_hold_key": null,
"s_nfc_dialog_remove_key": null,
"@_ndef": {}, "@_ndef": {},
"p_ndef_set_otp": "OTPコードがYubiKeyからクリップボードに正常にコピーされました。", "p_ndef_set_otp": "OTPコードがYubiKeyからクリップボードに正常にコピーされました。",

View File

@ -885,28 +885,95 @@
"l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB", "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB",
"s_allow_screenshots": "Zezwalaj na zrzuty ekranu", "s_allow_screenshots": "Zezwalaj na zrzuty ekranu",
"l_nfc_dialog_tap_key": null, "@_ndef_oath_actions": {},
"s_nfc_dialog_operation_success": "Powodzenie", "s_nfc_dialog_oath_reset": null,
"s_nfc_dialog_operation_failed": "Niepowodzenie", "s_nfc_dialog_oath_reset_processing": null,
"s_nfc_dialog_oath_reset_success": null,
"s_nfc_dialog_oath_reset_failure": null,
"s_nfc_dialog_oath_reset": "Działanie: resetuj aplet OATH", "s_nfc_dialog_oath_unlock": null,
"s_nfc_dialog_oath_unlock": "Działanie: odblokuj aplet OATH", "s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH", "s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH", "s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_oath_add_account": "Działanie: dodaj nowe konto",
"s_nfc_dialog_oath_rename_account": "Działanie: zmień nazwę konta",
"s_nfc_dialog_oath_delete_account": "Działanie: usuń konto",
"s_nfc_dialog_oath_calculate_code": "Działanie: oblicz kod OATH",
"s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się",
"s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont",
"s_nfc_dialog_oath_set_password": null,
"s_nfc_dialog_oath_change_password": null,
"s_nfc_dialog_oath_set_password_processing": null,
"s_nfc_dialog_oath_change_password_processing": null,
"s_nfc_dialog_oath_set_password_success": null,
"s_nfc_dialog_oath_change_password_success": null,
"s_nfc_dialog_oath_set_password_failure": null,
"s_nfc_dialog_oath_change_password_failure": null,
"s_nfc_dialog_oath_remove_password": null,
"s_nfc_dialog_oath_remove_password_processing": null,
"s_nfc_dialog_oath_remove_password_success": null,
"s_nfc_dialog_oath_remove_password_failure": null,
"s_nfc_dialog_oath_add_account": null,
"s_nfc_dialog_oath_add_account_processing": null,
"s_nfc_dialog_oath_add_account_success": null,
"s_nfc_dialog_oath_add_account_failure": null,
"s_nfc_dialog_oath_rename_account": null,
"s_nfc_dialog_oath_rename_account_processing": null,
"s_nfc_dialog_oath_rename_account_success": null,
"s_nfc_dialog_oath_rename_account_failure": null,
"s_nfc_dialog_oath_delete_account": null,
"s_nfc_dialog_oath_delete_account_processing": null,
"s_nfc_dialog_oath_delete_account_success": null,
"s_nfc_dialog_oath_delete_account_failure": null,
"s_nfc_dialog_oath_calculate_code": null,
"s_nfc_dialog_oath_calculate_code_processing": null,
"s_nfc_dialog_oath_calculate_code_success": null,
"s_nfc_dialog_oath_calculate_code_failure": null,
"s_nfc_dialog_oath_add_multiple_accounts": null,
"s_nfc_dialog_oath_add_multiple_accounts_processing": null,
"s_nfc_dialog_oath_add_multiple_accounts_success": null,
"s_nfc_dialog_oath_add_multiple_accounts_failure": null,
"@_ndef_fido_actions": {},
"s_nfc_dialog_fido_reset": null, "s_nfc_dialog_fido_reset": null,
"s_nfc_dialog_fido_reset_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": null,
"s_nfc_dialog_fido_unlock": null, "s_nfc_dialog_fido_unlock": null,
"l_nfc_dialog_fido_set_pin": null, "s_nfc_dialog_fido_unlock_processing": null,
"s_nfc_dialog_fido_unlock_success": null,
"s_nfc_dialog_fido_unlock_failure": null,
"s_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_set_pin_processing": null,
"s_nfc_dialog_fido_set_pin_success": null,
"s_nfc_dialog_fido_set_pin_failure": null,
"s_nfc_dialog_fido_change_pin": null,
"s_nfc_dialog_fido_change_pin_processing": null,
"s_nfc_dialog_fido_change_pin_success": null,
"s_nfc_dialog_fido_change_pin_failure": null,
"s_nfc_dialog_fido_delete_credential": null, "s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_fingerprint": null, "s_nfc_dialog_fido_delete_credential_processing": null,
"s_nfc_dialog_fido_rename_fingerprint": null, "s_nfc_dialog_fido_delete_credential_success": null,
"s_nfc_dialog_fido_failure": null, "s_nfc_dialog_fido_delete_credential_failure": null,
"@_ndef_operations": {},
"s_nfc_dialog_tap_for": null,
"@s_nfc_dialog_tap_for": {
"placeholders": {
"operation": {}
}
},
"s_nfc_dialog_read_key": null,
"s_nfc_dialog_read_key_failure": null,
"s_nfc_dialog_hold_key": null,
"s_nfc_dialog_remove_key": null,
"@_ndef": {}, "@_ndef": {},
"p_ndef_set_otp": "OTP zostało skopiowane do schowka.", "p_ndef_set_otp": "OTP zostało skopiowane do schowka.",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 Yubico. * Copyright (C) 2021-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.
@ -15,6 +15,7 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
const defaultPrimaryColor = Colors.lightGreen; const defaultPrimaryColor = Colors.lightGreen;
@ -50,6 +51,9 @@ class AppTheme {
fontFamily: 'Roboto', fontFamily: 'Roboto',
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
color: Colors.transparent, color: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent),
), ),
listTileTheme: const ListTileThemeData( listTileTheme: const ListTileThemeData(
// For alignment under menu button // For alignment under menu button
@ -81,6 +85,9 @@ class AppTheme {
scaffoldBackgroundColor: colorScheme.surface, scaffoldBackgroundColor: colorScheme.surface,
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
color: Colors.transparent, color: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent),
), ),
listTileTheme: const ListTileThemeData( listTileTheme: const ListTileThemeData(
// For alignment under menu button // For alignment under menu button

68
lib/widgets/pulsing.dart Normal file
View File

@ -0,0 +1,68 @@
/*
* 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.
*/
import 'package:flutter/material.dart';
class Pulsing extends StatefulWidget {
final Widget child;
const Pulsing({super.key, required this.child});
@override
State<Pulsing> createState() => _PulsingState();
}
class _PulsingState extends State<Pulsing> with SingleTickerProviderStateMixin {
late final AnimationController controller;
late final Animation<double> animationScale;
late final CurvedAnimation curvedAnimation;
static const _duration = Duration(milliseconds: 400);
@override
Widget build(BuildContext context) {
return SizedBox(
child: Transform.scale(scale: animationScale.value, child: widget.child),
);
}
@override
void initState() {
super.initState();
controller = AnimationController(
duration: _duration,
vsync: this,
);
curvedAnimation = CurvedAnimation(
parent: controller, curve: Curves.easeIn, reverseCurve: Curves.easeOut);
animationScale = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(curvedAnimation)
..addListener(() {
setState(() {});
});
controller.repeat(reverse: true);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}