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

View File

@ -17,7 +17,6 @@
package com.yubico.authenticator
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@ -355,13 +354,14 @@ class MainActivity : FlutterFragmentActivity() {
try {
it.processYubiKey(device)
if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
device.remove {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
}
}
} catch (e: Throwable) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
logger.error("Error processing YubiKey in AppContextManager", e)
}
}
}
@ -441,6 +441,7 @@ class MainActivity : FlutterFragmentActivity() {
oathViewModel,
dialogManager,
appPreferences,
appMethodChannel,
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
import com.yubico.authenticator.DialogManager
import com.yubico.authenticator.DialogTitle
import com.yubico.authenticator.device.DeviceManager
import com.yubico.authenticator.fido.data.YubiKitFidoSession
import com.yubico.authenticator.yubikit.withConnection
@ -48,12 +47,9 @@ class FidoConnectionHelper(
}
}
suspend fun <T> useSession(
actionDescription: FidoActionDescription,
action: (YubiKitFidoSession) -> T
): T {
suspend fun <T> useSession(action: (YubiKitFidoSession) -> T): T {
return deviceManager.withKey(
onNfc = { useSessionNfc(actionDescription,action) },
onNfc = { useSessionNfc(action) },
onUsb = { useSessionUsb(it, action) })
}
@ -64,10 +60,7 @@ class FidoConnectionHelper(
block(YubiKitFidoSession(it))
}
suspend fun <T> useSessionNfc(
actionDescription: FidoActionDescription,
block: (YubiKitFidoSession) -> T
): T {
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): T {
try {
val result = suspendCoroutine { outer ->
pendingAction = {
@ -75,11 +68,8 @@ class FidoConnectionHelper(
block.invoke(it.value)
})
}
dialogManager.showDialog(
DialogTitle.TapKey,
actionDescription.id
) {
logger.debug("Cancelled Dialog {}", actionDescription.name)
dialogManager.showDialog {
logger.debug("Cancelled dialog")
pendingAction?.invoke(Result.failure(CancellationException()))
pendingAction = null
}

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ pluginManagement {
plugins {
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.plugin.serialization" version "2.0.0" 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 '../../fido/models.dart';
import '../../fido/state.dart';
import '../tap_request_dialog.dart';
final _log = Logger('android.fido.state');
const _methods = MethodChannel('android.fido.methods');
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
class _FidoStateNotifier extends FidoStateNotifier {
final _events = const EventChannel('android.fido.sessionState');
late StreamSubscription _sub;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override
FutureOr<FidoState> build(DevicePath devicePath) async {
@ -79,7 +80,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
});
controller.onCancel = () async {
await _methods.invokeMethod('cancelReset');
await fido.cancelReset();
if (!controller.isClosed) {
await subscription.cancel();
}
@ -87,7 +88,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
controller.onListen = () async {
try {
await _methods.invokeMethod('reset');
await fido.reset();
await controller.sink.close();
ref.invalidateSelf();
} catch (e) {
@ -102,13 +103,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
try {
final response = jsonDecode(await _methods.invokeMethod(
'setPin',
{
'pin': oldPin,
'newPin': newPin,
},
));
final response = jsonDecode(await fido.setPin(newPin, oldPin: oldPin));
if (response['success'] == true) {
_log.debug('FIDO PIN set/change successful');
return PinResult.success();
@ -134,10 +129,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override
Future<PinResult> unlock(String pin) async {
try {
final response = jsonDecode(await _methods.invokeMethod(
'unlock',
{'pin': pin},
));
final response = jsonDecode(await fido.unlock(pin));
if (response['success'] == true) {
_log.debug('FIDO applet unlocked');
@ -165,9 +157,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
@override
Future<void> enableEnterpriseAttestation() async {
try {
final response = jsonDecode(await _methods.invokeMethod(
'enableEnterpriseAttestation',
));
final response = jsonDecode(await fido.enableEnterpriseAttestation());
if (response['success'] == true) {
_log.debug('Enterprise attestation enabled');
@ -193,6 +183,8 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
final _events = const EventChannel('android.fido.fingerprints');
late StreamSubscription _sub;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
@ -243,15 +235,14 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
controller.onCancel = () async {
if (!controller.isClosed) {
_log.debug('Cancelling fingerprint registration');
await _methods.invokeMethod('cancelRegisterFingerprint');
await fido.cancelFingerprintRegistration();
await registerFpSub.cancel();
}
};
controller.onListen = () async {
try {
final registerFpResult =
await _methods.invokeMethod('registerFingerprint', {'name': name});
final registerFpResult = await fido.registerFingerprint(name);
_log.debug('Finished registerFingerprint with: $registerFpResult');
@ -286,13 +277,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
Future<Fingerprint> renameFingerprint(
Fingerprint fingerprint, String name) async {
try {
final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod(
'renameFingerprint',
{
'templateId': fingerprint.templateId,
'name': name,
},
));
final renameFingerprintResponse =
jsonDecode(await fido.renameFingerprint(fingerprint, name));
if (renameFingerprintResponse['success'] == true) {
_log.debug('FIDO rename fingerprint succeeded');
@ -316,12 +302,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
@override
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
try {
final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod(
'deleteFingerprint',
{
'templateId': fingerprint.templateId,
},
));
final deleteFingerprintResponse =
jsonDecode(await fido.deleteFingerprint(fingerprint));
if (deleteFingerprintResponse['success'] == true) {
_log.debug('FIDO delete fingerprint succeeded');
@ -348,6 +330,8 @@ final androidCredentialProvider = AsyncNotifierProvider.autoDispose
class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
final _events = const EventChannel('android.fido.credentials');
late StreamSubscription _sub;
late final _FidoMethodChannelNotifier fido =
ref.read(_fidoMethodsProvider.notifier);
@override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
@ -371,13 +355,7 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
@override
Future<void> deleteCredential(FidoCredential credential) async {
try {
await _methods.invokeMethod(
'deleteCredential',
{
'rpId': credential.rpId,
'credentialId': credential.credentialId,
},
);
await fido.deleteCredential(credential);
} on PlatformException catch (pe) {
var decodedException = pe.decode();
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 'state.dart';
import 'tap_request_dialog.dart';
import 'views/nfc/nfc_activity_command_listener.dart';
import 'window_state_provider.dart';
Future<Widget> initialize() async {
@ -106,6 +107,8 @@ Future<Widget> initialize() async {
child: DismissKeyboard(
child: YubicoAuthenticatorApp(page: Consumer(
builder: (context, ref, child) {
ref.read(nfcActivityCommandListener).startListener(context);
Timer.run(() {
ref.read(featureFlagProvider.notifier)
// 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");
* 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 '../../oath/models.dart';
import '../../oath/state.dart';
import '../tap_request_dialog.dart';
final _log = Logger('android.oath.state');
const _methods = MethodChannel('android.oath.methods');
final androidOathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
_AndroidOathStateNotifier.new);
@ -47,6 +46,8 @@ final androidOathStateProvider = AsyncNotifierProvider.autoDispose
class _AndroidOathStateNotifier extends OathStateNotifier {
final _events = const EventChannel('android.oath.sessionState');
late StreamSubscription _sub;
late _OathMethodChannelNotifier oath =
ref.watch(_oathMethodsProvider.notifier);
@override
FutureOr<OathState> build(DevicePath arg) {
@ -75,7 +76,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
// await ref
// .read(androidAppContextHandler)
// .switchAppContext(Application.accounts);
await _methods.invokeMethod('reset');
await oath.reset();
} catch (e) {
_log.debug('Calling reset failed with exception: $e');
}
@ -84,8 +85,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override
Future<(bool, bool)> unlock(String password, {bool remember = false}) async {
try {
final unlockResponse = jsonDecode(await _methods.invokeMethod(
'unlock', {'password': password, 'remember': remember}));
final unlockResponse =
jsonDecode(await oath.unlock(password, remember: remember));
_log.debug('applet unlocked');
final unlocked = unlockResponse['unlocked'] == true;
@ -106,8 +107,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override
Future<bool> setPassword(String? current, String password) async {
try {
await _methods.invokeMethod(
'setPassword', {'current': current, 'password': password});
await oath.setPassword(current, password);
return true;
} on PlatformException catch (e) {
_log.debug('Calling set password failed with exception: $e');
@ -118,7 +118,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override
Future<bool> unsetPassword(String current) async {
try {
await _methods.invokeMethod('unsetPassword', {'current': current});
await oath.unsetPassword(current);
return true;
} on PlatformException catch (e) {
_log.debug('Calling unset password failed with exception: $e');
@ -129,7 +129,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
@override
Future<void> forgetPassword() async {
try {
await _methods.invokeMethod('forgetPassword');
await oath.forgetPassword();
} on PlatformException catch (e) {
_log.debug('Calling forgetPassword failed with exception: $e');
}
@ -161,12 +161,10 @@ Exception _decodeAddAccountException(PlatformException platformException) {
final addCredentialToAnyProvider =
Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async {
final oath = ref.watch(_oathMethodsProvider.notifier);
try {
String resultString = await _methods.invokeMethod(
'addAccountToAny', {
'uri': credentialUri.toString(),
'requireTouch': requireTouch
});
String resultString = await oath.addAccountToAny(credentialUri,
requireTouch: requireTouch);
var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']);
@ -177,17 +175,13 @@ final addCredentialToAnyProvider =
final addCredentialsToAnyProvider = Provider(
(ref) => (List<String> credentialUris, List<bool> touchRequired) async {
final oath = ref.read(_oathMethodsProvider.notifier);
try {
_log.debug(
'Calling android with ${credentialUris.length} credentials to be added');
String resultString = await _methods.invokeMethod(
'addAccountsToAny',
{
'uris': credentialUris,
'requireTouch': touchRequired,
},
);
String resultString =
await oath.addAccounts(credentialUris, touchRequired);
_log.debug('Call result: $resultString');
var result = jsonDecode(resultString);
@ -218,6 +212,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
final WithContext _withContext;
final Ref _ref;
late StreamSubscription _sub;
late _OathMethodChannelNotifier oath =
_ref.read(_oathMethodsProvider.notifier);
_AndroidCredentialListNotifier(this._withContext, this._ref) : super() {
_sub = _events.receiveBroadcastStream().listen((event) {
@ -264,8 +260,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
}
try {
final resultJson = await _methods
.invokeMethod('calculate', {'credentialId': credential.id});
final resultJson = await oath.calculate(credential);
_log.debug('Calculate', resultJson);
return OathCode.fromJson(jsonDecode(resultJson));
} on PlatformException catch (pe) {
@ -280,9 +275,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
Future<OathCredential> addAccount(Uri credentialUri,
{bool requireTouch = false}) async {
try {
String resultString = await _methods.invokeMethod('addAccount',
{'uri': credentialUri.toString(), 'requireTouch': requireTouch});
String resultString =
await oath.addAccount(credentialUri, requireTouch: requireTouch);
var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']);
} on PlatformException catch (pe) {
@ -294,9 +288,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
Future<OathCredential> renameAccount(
OathCredential credential, String? issuer, String name) async {
try {
final response = await _methods.invokeMethod('renameAccount',
{'credentialId': credential.id, 'name': name, 'issuer': issuer});
final response = await oath.renameAccount(credential, issuer, name);
_log.debug('Rename response: $response');
var responseJson = jsonDecode(response);
@ -311,11 +303,149 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
@override
Future<void> deleteAccount(OathCredential credential) async {
try {
await _methods
.invokeMethod('deleteAccount', {'credentialId': credential.id});
await oath.deleteAccount(credential);
} on PlatformException catch (e) {
_log.debug('Received exception: $e');
throw e.decode();
var decoded = 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:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/views/user_interaction.dart';
import 'views/nfc/nfc_activity_widget.dart';
import '../widgets/pulsing.dart';
import 'state.dart';
import 'views/nfc/nfc_activity_overlay.dart';
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
// _DDesc contains id of title resource for the dialog
enum _DTitle {
tapKey,
operationSuccessful,
operationFailed,
invalid;
final androidDialogProvider =
NotifierProvider<_DialogProvider, int>(_DialogProvider.new);
static _DTitle fromId(int? id) =>
const {
0: _DTitle.tapKey,
1: _DTitle.operationSuccessful,
2: _DTitle.operationFailed
}[id] ??
_DTitle.invalid;
}
class _DialogProvider extends Notifier<int> {
Timer? processingTimer;
bool explicitAction = false;
// _DDesc contains action description in the dialog
enum _DDesc {
// oath descriptions
oathResetApplet,
oathUnlockSession,
oathSetPassword,
oathUnsetPassword,
oathAddAccount,
oathRenameAccount,
oathDeleteAccount,
oathCalculateCode,
oathActionFailure,
oathAddMultipleAccounts,
// FIDO descriptions
fidoResetApplet,
fidoUnlockSession,
fidoSetPin,
fidoDeleteCredential,
fidoDeleteFingerprint,
fidoRenameFingerprint,
fidoRegisterFingerprint,
fidoEnableEnterpriseAttestation,
fidoActionFailure,
// Others
invalid;
@override
int build() {
final l10n = ref.read(l10nProvider);
ref.listen(androidNfcActivityProvider, (previous, current) {
final notifier = ref.read(nfcActivityCommandNotifier.notifier);
static const int dialogDescriptionOathIndex = 100;
static const int dialogDescriptionFidoIndex = 200;
if (!explicitAction) {
// 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) =>
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 properties = ref.read(nfcActivityWidgetNotifier);
final androidDialogProvider = Provider<_DialogProvider>(
(ref) {
return _DialogProvider(ref.watch(withContextProvider));
},
);
debugPrint('XXX now it is: $current');
switch (current) {
case NfcActivity.processingStarted:
processingTimer?.cancel();
class _DialogProvider {
final WithContext _withContext;
final Widget _icon = const NfcActivityWidget(width: 64, height: 64);
UserInteractionController? _controller;
debugPrint('XXX explicit action: $explicitAction');
final timeout = explicitAction ? 300 : 200;
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 {
final args = jsonDecode(call.arguments);
final notifier = ref.read(nfcActivityCommandNotifier.notifier);
final properties = ref.read(nfcActivityWidgetNotifier);
switch (call.method) {
case 'close':
_closeDialog();
break;
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;
case 'state':
await _updateDialogState(args['title'], args['description']);
case 'close':
notifier.update(NfcActivityWidgetCommand(
action: const NfcActivityWidgetActionHideWidget(timeoutMs: 0)));
break;
default:
throw PlatformException(
code: 'NotImplemented',
@ -129,71 +148,112 @@ class _DialogProvider {
);
}
});
return 0;
}
void _closeDialog() {
_controller?.close();
_controller = null;
void cancelDialog() async {
debugPrint('Cancelled dialog');
explicitAction = false;
await _channel.invokeMethod('cancel');
}
String _getTitle(BuildContext context, int? titleId) {
final l10n = AppLocalizations.of(context)!;
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,
_ => ''
};
}
Future<void> waitForDialogClosed() async {
final completer = Completer();
String _getDialogDescription(BuildContext context, int? descriptionId) {
final l10n = AppLocalizations.of(context)!;
return switch (_DDesc.fromId(descriptionId)) {
_DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset,
_DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock,
_DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password,
_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,
_ => ''
};
}
Timer.periodic(
const Duration(milliseconds: 200),
(timer) {
if (!ref.read(nfcActivityWidgetNotifier.select((s) => s.isShowing))) {
timer.cancel();
completer.complete();
}
},
);
Future<void> _updateDialogState(int? title, int? description) async {
await _withContext((context) async {
_controller?.updateContent(
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: (_DDesc.fromId(description) != _DDesc.oathActionFailure)
? _icon
: const Icon(Icons.warning_amber_rounded, size: 64),
);
});
}
Future<void> _showDialog(int title, int description) async {
_controller = await _withContext((context) async {
return promptUserInteraction(
context,
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: _icon,
onCancel: () {
_channel.invokeMethod('cancel');
},
);
});
await completer.future;
}
}
class _NfcActivityWidgetView extends StatelessWidget {
final bool inProgress;
final String? title;
final String? subtitle;
const _NfcActivityWidgetView(
{required this.title, this.subtitle, this.inProgress = false});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
Text(title ?? 'Missing title',
textAlign: TextAlign.center,
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
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 =>
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",
"s_allow_screenshots": "Bildschirmfotos erlauben",
"l_nfc_dialog_tap_key": "Halten Sie Ihren Schlüssel dagegen",
"s_nfc_dialog_operation_success": "Erfolgreich",
"s_nfc_dialog_operation_failed": "Fehlgeschlagen",
"@_ndef_oath_actions": {},
"s_nfc_dialog_oath_reset": null,
"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": "Aktion: OATH-Anwendung entsperren",
"s_nfc_dialog_oath_set_password": "Aktion: OATH-Passwort setzen",
"s_nfc_dialog_oath_unset_password": "Aktion: OATH-Passwort entfernen",
"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_oath_unlock": null,
"s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_fido_reset": "Aktion: FIDO-Anwendung zurücksetzen",
"s_nfc_dialog_fido_unlock": "Aktion: FIDO-Anwendung entsperren",
"l_nfc_dialog_fido_set_pin": "Aktion: FIDO-PIN setzen oder ändern",
"s_nfc_dialog_fido_delete_credential": "Aktion: Passkey löschen",
"s_nfc_dialog_fido_delete_fingerprint": "Aktion: Fingerabdruck löschen",
"s_nfc_dialog_fido_rename_fingerprint": "Aktion: Fingerabdruck umbenennen",
"s_nfc_dialog_fido_failure": "FIDO-Operation fehlgeschlagen",
"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_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": {},
"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",
"s_allow_screenshots": "Allow screenshots",
"l_nfc_dialog_tap_key": "Tap and hold your key",
"s_nfc_dialog_operation_success": "Success",
"s_nfc_dialog_operation_failed": "Failed",
"@_ndef_oath_actions": {},
"s_nfc_dialog_oath_reset": "reset Accounts",
"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": "Action: unlock OATH application",
"s_nfc_dialog_oath_set_password": "Action: set OATH password",
"s_nfc_dialog_oath_unset_password": "Action: remove OATH password",
"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_oath_unlock": "unlock",
"s_nfc_dialog_oath_unlock_processing": "Unlocking",
"s_nfc_dialog_oath_unlock_success": "Accounts unlocked",
"s_nfc_dialog_oath_unlock_failure": "Failed to unlock",
"s_nfc_dialog_fido_reset": "Action: reset FIDO application",
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO application",
"l_nfc_dialog_fido_set_pin": "Action: set or change the FIDO PIN",
"s_nfc_dialog_fido_delete_credential": "Action: delete Passkey",
"s_nfc_dialog_fido_delete_fingerprint": "Action: delete fingerprint",
"s_nfc_dialog_fido_rename_fingerprint": "Action: rename fingerprint",
"s_nfc_dialog_fido_failure": "FIDO operation failed",
"s_nfc_dialog_oath_set_password": "set password",
"s_nfc_dialog_oath_change_password": "change password",
"s_nfc_dialog_oath_set_password_processing": "Setting password",
"s_nfc_dialog_oath_change_password_processing": "Changing password",
"s_nfc_dialog_oath_set_password_success": "Password set",
"s_nfc_dialog_oath_change_password_success": "Password changed",
"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": {},
"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",
"s_allow_screenshots": "Autoriser captures d'écran",
"l_nfc_dialog_tap_key": "Appuyez et maintenez votre clé",
"s_nfc_dialog_operation_success": "Succès",
"s_nfc_dialog_operation_failed": "Échec",
"@_ndef_oath_actions": {},
"s_nfc_dialog_oath_reset": null,
"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": "Action\u00a0: débloquer applet OATH",
"s_nfc_dialog_oath_set_password": "Action\u00a0: définir mot de passe OATH",
"s_nfc_dialog_oath_unset_password": "Action\u00a0: supprimer mot de passe OATH",
"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_oath_unlock": null,
"s_nfc_dialog_oath_unlock_processing": null,
"s_nfc_dialog_oath_unlock_success": null,
"s_nfc_dialog_oath_unlock_failure": null,
"s_nfc_dialog_fido_reset": "Action : réinitialiser l'application FIDO",
"s_nfc_dialog_fido_unlock": "Action : déverrouiller l'application FIDO",
"l_nfc_dialog_fido_set_pin": "Action : définir ou modifier le code PIN FIDO",
"s_nfc_dialog_fido_delete_credential": "Action : supprimer le Passkey",
"s_nfc_dialog_fido_delete_fingerprint": "Action : supprimer l'empreinte digitale",
"s_nfc_dialog_fido_rename_fingerprint": "Action : renommer l'empreinte digitale",
"s_nfc_dialog_fido_failure": "Échec de l'opération FIDO",
"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_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": {},
"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を使用できます",
"s_allow_screenshots": "スクリーンショットを許可",
"l_nfc_dialog_tap_key": "キーをタップして長押しします",
"s_nfc_dialog_operation_success": "成功",
"s_nfc_dialog_operation_failed": "失敗",
"@_ndef_oath_actions": {},
"s_nfc_dialog_oath_reset": "アクションOATHアプレットをリセット",
"s_nfc_dialog_oath_unlock": "アクションOATHアプレットをロック解除",
"s_nfc_dialog_oath_set_password": "アクションOATHパスワードを設定",
"s_nfc_dialog_oath_unset_password": "アクションOATHパスワードを削除",
"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_reset_processing": null,
"s_nfc_dialog_oath_reset_success": null,
"s_nfc_dialog_oath_reset_failure": null,
"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_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": null,
"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_fingerprint": "アクション: 指紋の削除",
"s_nfc_dialog_fido_rename_fingerprint": "アクション: 指紋の名前を変更する",
"s_nfc_dialog_fido_failure": "FIDO操作に失敗しました",
"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": {},
"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",
"s_allow_screenshots": "Zezwalaj na zrzuty ekranu",
"l_nfc_dialog_tap_key": null,
"s_nfc_dialog_operation_success": "Powodzenie",
"s_nfc_dialog_operation_failed": "Niepowodzenie",
"@_ndef_oath_actions": {},
"s_nfc_dialog_oath_reset": null,
"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": "Działanie: odblokuj aplet OATH",
"s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH",
"s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH",
"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_unlock": null,
"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": 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_processing": null,
"s_nfc_dialog_fido_reset_success": null,
"s_nfc_dialog_fido_reset_failure": 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_fingerprint": null,
"s_nfc_dialog_fido_rename_fingerprint": null,
"s_nfc_dialog_fido_failure": 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": {},
"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");
* you may not use this file except in compliance with the License.
@ -15,6 +15,7 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
const defaultPrimaryColor = Colors.lightGreen;
@ -50,6 +51,9 @@ class AppTheme {
fontFamily: 'Roboto',
appBarTheme: const AppBarTheme(
color: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent),
),
listTileTheme: const ListTileThemeData(
// For alignment under menu button
@ -81,6 +85,9 @@ class AppTheme {
scaffoldBackgroundColor: colorScheme.surface,
appBarTheme: const AppBarTheme(
color: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent),
),
listTileTheme: const ListTileThemeData(
// 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();
}
}