diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt index f6cf0eea..425f774d 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt @@ -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) } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 62fa7546..fe941f4d 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -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 ) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt deleted file mode 100644 index ae0d8945..00000000 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt index adc2bee7..445067f2 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt @@ -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 useSession( - actionDescription: FidoActionDescription, - action: (YubiKitFidoSession) -> T - ): T { + suspend fun 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 useSessionNfc( - actionDescription: FidoActionDescription, - block: (YubiKitFidoSession) -> T - ): T { + suspend fun 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 } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt index 6919c2cb..09b98cb1 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt @@ -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) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt index a89a8526..d7f4d367 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt @@ -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) } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt index 24ac7b85..4c82c856 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt @@ -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 diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt deleted file mode 100644 index ac78d2c5..00000000 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index 44e5bdcb..1a5802f1 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -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 { 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 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 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() diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt index 60c45ab9..b827605f 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt @@ -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 = diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcActivityDispatcher.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcActivityDispatcher.kt index 0bb14899..cea74967 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcActivityDispatcher.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcActivityDispatcher.kt @@ -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) } diff --git a/android/settings.gradle b/android/settings.gradle index 5c254c38..e462de1b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -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 diff --git a/lib/android/fido/state.dart b/lib/android/fido/state.dart index 956c58b7..b7cc7c88 100644 --- a/lib/android/fido/state.dart +++ b/lib/android/fido/state.dart @@ -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.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 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 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 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 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> 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 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 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> build(DevicePath devicePath) async { @@ -371,13 +355,7 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier { @override Future 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 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 cancelReset() async => invoke('cancelReset'); + + Future 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 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 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 enableEnterpriseAttestation() async => + invoke('enableEnterpriseAttestation'); + + Future registerFingerprint(String? name) async => + invoke('registerFingerprint', { + 'callArgs': {'name': name} + }); + + Future cancelFingerprintRegistration() async => + invoke('cancelRegisterFingerprint'); + + Future renameFingerprint( + Fingerprint fingerprint, String name) async => + invoke('renameFingerprint', { + 'callArgs': {'templateId': fingerprint.templateId, 'name': name}, + }); + + Future deleteFingerprint(Fingerprint fingerprint) async => + invoke('deleteFingerprint', { + 'callArgs': {'templateId': fingerprint.templateId}, + }); +} diff --git a/lib/android/init.dart b/lib/android/init.dart index 6322acd0..607067ab 100644 --- a/lib/android/init.dart +++ b/lib/android/init.dart @@ -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 initialize() async { @@ -106,6 +107,8 @@ Future 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? diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index 03b0bdcf..fbcc6c3e 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -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( _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 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 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 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 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 credentialUris, List 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 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 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 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 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 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 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 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 forgetPassword() async => invoke('forgetPassword'); + + Future 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 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 addAccounts( + List credentialUris, List 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 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 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 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, + }); +} diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart index 66fc7fb0..ccc89aea 100755 --- a/lib/android/tap_request_dialog.dart +++ b/lib/android/tap_request_dialog.dart @@ -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 { + 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 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 _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 _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 invoke(String method, + {String? operationName, + String? operationSuccess, + String? operationProcessing, + String? operationFailure, + bool? showSuccess, + Map 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 { + final MethodChannel _channel; + + MethodChannelNotifier(this._channel); + + @override + void build() {} + + Future invoke(String name, + [Map 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; } } diff --git a/lib/android/views/nfc/nfc_activity_command_listener.dart b/lib/android/views/nfc/nfc_activity_command_listener.dart new file mode 100644 index 00000000..c25d2d3f --- /dev/null +++ b/lib/android/views/nfc/nfc_activity_command_listener.dart @@ -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? 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); + } + } +} diff --git a/lib/android/views/nfc/nfc_activity_overlay.dart b/lib/android/views/nfc/nfc_activity_overlay.dart new file mode 100644 index 00000000..985b016e --- /dev/null +++ b/lib/android/views/nfc/nfc_activity_overlay.dart @@ -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 { + @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 createState() => + _NfcActivityClosingCountdownWidgetViewState(); +} + +class _NfcActivityClosingCountdownWidgetViewState + extends ConsumerState { + 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 { + @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), + ], + ); + } +} diff --git a/lib/app/models.dart b/lib/app/models.dart index 1f45ebab..433e3ab3 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -170,3 +170,46 @@ class _ColorConverter implements JsonConverter { @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; +} diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index 37629fcb..52854fb3 100644 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -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 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 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; +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9400d885..cf2314cb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9954b87e..a3e534d7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9e43e088..5089233d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 09ac00a0..2675abf7 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -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からクリップボードに正常にコピーされました。", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 50623db1..ad1fa188 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -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.", diff --git a/lib/theme.dart b/lib/theme.dart index d929fa31..b2d82e13 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -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 diff --git a/lib/widgets/pulsing.dart b/lib/widgets/pulsing.dart new file mode 100644 index 00000000..9e941b1a --- /dev/null +++ b/lib/widgets/pulsing.dart @@ -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 createState() => _PulsingState(); +} + +class _PulsingState extends State with SingleTickerProviderStateMixin { + late final AnimationController controller; + late final Animation 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( + begin: 1.0, + end: 1.2, + ).animate(curvedAnimation) + ..addListener(() { + setState(() {}); + }); + + controller.repeat(reverse: true); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +}