From 955d1a2aabdbf23c43fa195259aad3e97f0b9c98 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 6 May 2026 16:14:27 +0300 Subject: [PATCH] Add CAN lock message on wrong CAN input --- .../domain/preferences/DataStoreTest.kt | 16 ++ .../ria/DigiDoc/viewmodel/NFCViewModelTest.kt | 68 +++++-- .../DigiDoc/domain/preferences/DataStore.kt | 7 + .../ee/ria/DigiDoc/exceptions/NFCError.kt | 81 ++++++++ .../fragment/screen/DiagnosticsScreen.kt | 2 +- .../myeid/pinandcertificate/MyEidPinScreen.kt | 58 +++--- .../ui/component/shared/HrefMessageDialog.kt | 2 +- .../component/shared/dialog/WrongCanDialog.kt | 175 +++++++++++++++++ .../signing/NFCSignatureUpdateContainer.kt | 17 +- .../DigiDoc/ui/component/signing/NFCView.kt | 44 +++-- .../ee/ria/DigiDoc/viewmodel/NFCViewModel.kt | 178 +++++++++--------- .../shared/SharedSettingsViewModel.kt | 6 + app/src/main/res/values-et/strings.xml | 7 +- app/src/main/res/values/strings.xml | 7 +- .../kotlin/ee/ria/DigiDoc/common/Constant.kt | 2 + 15 files changed, 513 insertions(+), 157 deletions(-) create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/WrongCanDialog.kt diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt index 84f048b2f..9a87a44e6 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt @@ -614,4 +614,20 @@ class DataStoreTest { assertFalse(result) } + + @Test + fun dataStore_setDoNotShowWrongCanDialog_success() { + dataStore.setDoNotShowWrongCanDialog(true) + + val result = dataStore.getDoNotShowWrongCanDialog() + + assertTrue(result) + } + + @Test + fun dataStore_getDoNotShowWrongCanDialog_success() { + val result = dataStore.getDoNotShowWrongCanDialog() + + assertFalse(result) + } } diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt index 22127b38d..c0eae8785 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt @@ -33,7 +33,9 @@ import ee.ria.DigiDoc.common.testfiles.asset.AssetFile import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository import ee.ria.DigiDoc.cryptolib.CDOC2Settings import ee.ria.DigiDoc.domain.model.IdCardData +import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.domain.service.IdCardService +import ee.ria.DigiDoc.exceptions.NFCError import ee.ria.DigiDoc.idcard.CodeType import ee.ria.DigiDoc.libdigidoclib.SignedContainer import ee.ria.DigiDoc.libdigidoclib.domain.model.ContainerWrapper @@ -43,10 +45,16 @@ import ee.ria.DigiDoc.libdigidoclib.init.LibdigidocLibraryLoader import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager.NfcStatus import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before @@ -90,6 +98,8 @@ class NFCViewModelTest { private lateinit var configurationRepository: ConfigurationRepository + private lateinit var dataStore: DataStore + @Mock private lateinit var idCardService: IdCardService @@ -118,6 +128,7 @@ class NFCViewModelTest { containerWrapper = ContainerWrapperImpl() configurationRepository = mock(ConfigurationRepository::class.java) cdoc2Settings = CDOC2Settings(context, configurationRepository) + dataStore = DataStore(context) viewModel = NFCViewModel( @@ -126,6 +137,7 @@ class NFCViewModelTest { cdoc2Settings, configurationRepository, idCardService, + dataStore, ) scenario = ActivityScenario.launch(ComponentActivity::class.java) @@ -181,16 +193,29 @@ class NFCViewModelTest { viewModel.signStatus.removeObserver(resetSignStatusObserver) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun nfcViewModel_resetErrorState_success() = runTest { - val errorStateObserver: Observer?> = mock() - viewModel.errorState.observeForever(errorStateObserver) + val values = mutableListOf() + val job = launch { viewModel.errorState.toList(values) } + + viewModel.performNFCSignWorkRequest( + activity = activity, + context = context, + container = null, + pin2Code = byteArrayOf(), + canNumber = "", + roleData = null, + ) viewModel.resetErrorState() - verify(errorStateObserver, atLeastOnce()).onChanged(null) - viewModel.errorState.removeObserver(errorStateObserver) + advanceUntilIdle() + job.cancel() + + assertTrue(values.isNotEmpty()) + assertNull(values.last()) } @Test @@ -392,6 +417,9 @@ class NFCViewModelTest { runTest { val signedContainer = SignedContainer.openOrCreate(context, container, listOf(container), true) + val values = mutableListOf() + val job = launch { viewModel.errorState.toList(values) } + viewModel.performNFCSignWorkRequest( activity, context, @@ -401,10 +429,9 @@ class NFCViewModelTest { null, ) - val errorStateObserver: Observer?> = mock() - viewModel.errorState.observeForever(errorStateObserver) - verify(errorStateObserver, atLeastOnce()).onChanged(null) - viewModel.errorState.removeObserver(errorStateObserver) + job.cancel() + + assertNull(values.last()) val signStatusObserver: Observer = mock() viewModel.signStatus.observeForever(signStatusObserver) @@ -420,12 +447,17 @@ class NFCViewModelTest { @Test fun nfcViewModel_performNFCSignWorkRequest_nullContainer() = runTest { + val values = mutableListOf() + val job = launch { viewModel.errorState.toList(values) } + viewModel.performNFCSignWorkRequest(activity, context, null, byteArrayOf(1, 1, 5, 5, 5), "444222", null) - val errorStateObserver: Observer?> = mock() - viewModel.errorState.observeForever(errorStateObserver) - verify(errorStateObserver, atLeastOnce()).onChanged(Triple(R.string.error_general_client, null, null)) - viewModel.errorState.removeObserver(errorStateObserver) + job.cancel() + + assertTrue(values.isNotEmpty()) + val error = values.last() + assertNotNull(error) + assertEquals(R.string.error_general_client, error?.message) } @Test @@ -469,15 +501,19 @@ class NFCViewModelTest { viewModel.dialogError.removeObserver(errorStateObserver) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun nfcViewModel_loadPersonalData_success() = runTest { + val values = mutableListOf() + val job = launch { viewModel.errorState.toList(values) } + viewModel.loadPersonalData(activity, "123456") - val errorStateObserver: Observer?> = mock() - viewModel.errorState.observeForever(errorStateObserver) - verify(errorStateObserver, atLeastOnce()).onChanged(null) - viewModel.errorState.removeObserver(errorStateObserver) + advanceUntilIdle() + job.cancel() + + assertNull(values.last()) val userDataObserver: Observer = mock() viewModel.userData.observeForever(userDataObserver) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt index ca29ff3a4..fcc23e1ca 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt @@ -32,6 +32,7 @@ import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE import ee.ria.DigiDoc.common.Constant.IS_CRASH_SENDING_ALWAYS_ENABLED import ee.ria.DigiDoc.common.Constant.KEY_LOCALE import ee.ria.DigiDoc.common.Constant.MyEID.IDENTIFICATION_METHOD_SETTING +import ee.ria.DigiDoc.common.Constant.NFCConstants.DO_NOT_SHOW_WRONG_CAN_DIALOG import ee.ria.DigiDoc.common.Constant.Theme.THEME_SETTING import ee.ria.DigiDoc.common.preferences.EncryptedPreferences import ee.ria.DigiDoc.domain.model.crypto.DecryptMethodSetting @@ -772,6 +773,12 @@ class DataStore preferences.edit { putString(IDENTIFICATION_METHOD_SETTING, myEidIdentificationMethodSetting.methodName) } } + fun getDoNotShowWrongCanDialog(): Boolean = preferences.getBoolean(DO_NOT_SHOW_WRONG_CAN_DIALOG, false) + + fun setDoNotShowWrongCanDialog(doNotShowAgain: Boolean) { + preferences.edit { putBoolean(DO_NOT_SHOW_WRONG_CAN_DIALOG, doNotShowAgain) } + } + private fun getEncryptedPreferences(context: Context): SharedPreferences? = try { EncryptedPreferences.getEncryptedPreferences(context) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt new file mode 100644 index 000000000..25880619f --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.exceptions + +sealed class NFCError { + abstract val message: Int + + data class TagLost( + override val message: Int, + ) : NFCError() + + data class WrongPin( + val pinType: String, + val retriesLeft: Int, + override val message: Int, + ) : NFCError() + + data class PinBlocked( + val pinType: String, + override val message: Int, + ) : NFCError() + + data class ApduResponse( + override val message: Int, + ) : NFCError() + + data class WrongCan( + override val message: Int, + ) : NFCError() + + data class LimitExceeded( + override val message: Int, + ) : NFCError() + + data class NoInternetConnection( + override val message: Int, + ) : NFCError() + + data class NoProxyConnection( + override val message: Int, + ) : NFCError() + + data class NoLockFound( + override val message: Int, + ) : NFCError() + + data class CertificateRevoked( + override val message: Int, + ) : NFCError() + + data class CertificateUnknown( + override val message: Int, + ) : NFCError() + + data class TechnicalError( + override val message: Int, + ) : NFCError() + + data class GeneralError( + override val message: Int, + ) : NFCError() +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt index 853db5ca6..f8cd5b4dd 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt @@ -664,7 +664,7 @@ fun DiagnosticsScreen( HrefMessageDialog( text1 = R.string.main_diagnostics_restart_message, text2 = R.string.main_diagnostics_restart_message_restart_now, - linkText = R.string.main_diagnostics_restart_message_read_more, + linkText = R.string.read_more_here, linkUrl = R.string.main_diagnostics_restart_message_href, newLineBeforeLink = true, newLineBeforeText2 = true, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt index ba56162a0..cbb30e670 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt @@ -389,18 +389,23 @@ fun MyEidPinScreen( } BackHandler { - if (showNewRepeatPinField.value) { - newPinRepeatedState.value = byteArrayOf() - - showNewRepeatPinField.value = false - showCurrentPinField.value = false - showNewPinField.value = true - } else if (showCurrentPinField.value) { - resetPins() - sharedMyEidViewModel.resetScreenContent() - navController.navigateUp() - } else { + if (showNFCScreen.value) { + showNFCScreen.value = false resetToBeginning() + } else { + if (showNewRepeatPinField.value) { + newPinRepeatedState.value = byteArrayOf() + + showNewRepeatPinField.value = false + showCurrentPinField.value = false + showNewPinField.value = true + } else if (showCurrentPinField.value) { + resetPins() + sharedMyEidViewModel.resetScreenContent() + navController.navigateUp() + } else { + resetToBeginning() + } } } @@ -434,22 +439,27 @@ fun MyEidPinScreen( R.string.back }, onLeftButtonClick = { - if (showNewRepeatPinField.value) { - showNewRepeatPinField.value = false - showCurrentPinField.value = false - showNewPinField.value = true - - newPinRepeatedState.value = byteArrayOf() - } else if (showNewPinField.value) { + if (showNFCScreen.value) { + showNFCScreen.value = false resetToBeginning() + } else { + if (showNewRepeatPinField.value) { + showNewRepeatPinField.value = false + showCurrentPinField.value = false + showNewPinField.value = true - newPinRepeatedState.value = byteArrayOf() + newPinRepeatedState.value = byteArrayOf() + } else if (showNewPinField.value) { + resetToBeginning() - newPinState.value = byteArrayOf() - } else { - resetPins() - sharedMyEidViewModel.resetScreenContent() - navController.navigateUp() + newPinRepeatedState.value = byteArrayOf() + + newPinState.value = byteArrayOf() + } else { + resetPins() + sharedMyEidViewModel.resetScreenContent() + navController.navigateUp() + } } }, onRightSecondaryButtonClick = { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/HrefMessageDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/HrefMessageDialog.kt index b88bdbeac..7147d6689 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/HrefMessageDialog.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/HrefMessageDialog.kt @@ -77,7 +77,7 @@ fun HrefMessageDialogPreview() { HrefMessageDialog( text1 = R.string.main_diagnostics_restart_message, text2 = R.string.main_diagnostics_restart_message_restart_now, - linkText = R.string.main_diagnostics_restart_message_read_more, + linkText = R.string.read_more_here, linkUrl = R.string.main_diagnostics_restart_message_href, ) } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/WrongCanDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/WrongCanDialog.kt new file mode 100644 index 000000000..f8a0af3b5 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/WrongCanDialog.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.ui.component.shared.dialog + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Checkbox +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.zeroPadding +import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme +import ee.ria.DigiDoc.utils.extensions.notAccessible + +@Composable +fun WrongCanDialog(onDismiss: (doNotShowAgain: Boolean) -> Unit) { + var doNotShowAgain by remember { mutableStateOf(false) } + + val message = stringResource(R.string.signature_update_nfc_wrong_can_message) + val url = stringResource(R.string.signature_update_nfc_wrong_can_url) + val doNotShowAgainMessage = stringResource(R.string.do_not_show_again) + val dialog = stringResource(R.string.dialog) + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + val annotatedString = + buildAnnotatedString { + append(message) + append("\n") + pushLink( + LinkAnnotation.Url( + url = url, + styles = + TextLinkStyles( + style = + SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + ), + ), + ), + ) + append(stringResource(R.string.read_more_here)) + pop() + } + + Dialog(onDismissRequest = { onDismiss(doNotShowAgain) }) { + Surface( + modifier = Modifier.semantics { paneTitle = dialog }, + shape = RoundedCornerShape(SPadding) + ) { + Column(modifier = Modifier.padding(MPadding)) { + Text( + modifier = + Modifier + .padding(top = SPadding) + .focusRequester(focusRequester) + .focusable(), + text = annotatedString, + ) + + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides zeroPadding) { + Row( + modifier = + Modifier + .padding(top = SPadding) + .clickable { doNotShowAgain = !doNotShowAgain } + .semantics(mergeDescendants = true) { + toggleableState = ToggleableState(doNotShowAgain) + role = Role.Checkbox + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + modifier = + Modifier.semantics { + contentDescription = doNotShowAgainMessage + }, + checked = doNotShowAgain, + onCheckedChange = null, + ) + Text( + modifier = + Modifier + .padding(horizontal = XSPadding) + .notAccessible(), + text = doNotShowAgainMessage, + ) + } + } + TextButton( + modifier = + Modifier + .align(Alignment.End) + .padding(top = MPadding), + onClick = { onDismiss(doNotShowAgain) }, + ) { + Text(stringResource(R.string.ok_button)) + } + } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun WrongCanDialogPreview() { + RIADigiDocTheme { + WrongCanDialog( + onDismiss = {}, + ) + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCSignatureUpdateContainer.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCSignatureUpdateContainer.kt index ff1149f81..bad244db3 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCSignatureUpdateContainer.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCSignatureUpdateContainer.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.asFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ee.ria.DigiDoc.R import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding @@ -79,6 +80,8 @@ fun NFCSignatureUpdateContainer( val context = LocalContext.current val focusRequester = remember { FocusRequester() } + val errorState by nfcViewModel.errorState.collectAsStateWithLifecycle() + val nfcDialogDefaultText = stringResource(id = R.string.signature_update_nfc_hold) var nfcDialogText by remember { mutableStateOf(nfcDialogDefaultText) } @@ -94,16 +97,10 @@ fun NFCSignatureUpdateContainer( } } - LaunchedEffect(nfcViewModel.errorState) { - nfcViewModel.errorState.asFlow().collect { error -> - error?.let { - context.getString( - error.first, - error.second, - error.third, - ) - onError() - } + LaunchedEffect(errorState) { + errorState?.let { error -> + context.getString(error.message) + onError() } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt index 70f8f0142..6518b28f9 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt @@ -91,6 +91,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.asFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.NFCConstants.CAN_LENGTH import ee.ria.DigiDoc.common.Constant.NFCConstants.PIN1_MIN_LENGTH @@ -98,6 +99,7 @@ import ee.ria.DigiDoc.common.Constant.NFCConstants.PIN2_MIN_LENGTH import ee.ria.DigiDoc.common.Constant.NFCConstants.PIN_MAX_LENGTH import ee.ria.DigiDoc.domain.model.IdCardData import ee.ria.DigiDoc.domain.model.IdentityAction +import ee.ria.DigiDoc.exceptions.NFCError import ee.ria.DigiDoc.idcard.CodeType import ee.ria.DigiDoc.libdigidoclib.domain.model.RoleData import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager.NfcStatus @@ -106,6 +108,7 @@ import ee.ria.DigiDoc.ui.component.shared.HrefMessageDialog import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.RoleDataView import ee.ria.DigiDoc.ui.component.shared.SecurePinTextField +import ee.ria.DigiDoc.ui.component.shared.dialog.WrongCanDialog import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -183,8 +186,13 @@ fun NFCView( ), ) } + val errorState by nfcViewModel.errorState.collectAsStateWithLifecycle() var errorText by remember { mutableStateOf("") } val showErrorDialog = rememberSaveable { mutableStateOf(false) } + var showWrongCanDialog by remember { mutableStateOf(false) } + var doNotShowWrongCanDialogAgain by remember { + mutableStateOf(nfcViewModel.getDoNotShowWrongCanDialog()) + } val focusManager = LocalFocusManager.current val saveFormParams = { if (shouldRememberMe) { @@ -273,23 +281,19 @@ fun NFCView( } } - LaunchedEffect(nfcViewModel.errorState) { - nfcViewModel.errorState.asFlow().collect { errorState -> - errorState?.let { - withContext(Main) { - pinCode.value.fill(0) - if (errorState.first != 0) { - errorText = - context.getString( - errorState.first, - errorState.second, - errorState.third, - ) - } + LaunchedEffect(errorState) { + errorState?.let { error -> + if (error is NFCError.WrongCan && !doNotShowWrongCanDialogAgain) showWrongCanDialog = true - nfcViewModel.resetErrorState() + errorText = when(error) { + is NFCError.PinBlocked -> context.getString(error.message, error.pinType) + is NFCError.WrongPin -> context.getString(error.message, error.pinType, error.retriesLeft) + else -> { + context.getString(error.message) } } + + nfcViewModel.resetErrorState() } } @@ -431,6 +435,18 @@ fun NFCView( } } + if (showWrongCanDialog) { + WrongCanDialog( + onDismiss = { shouldNotShowDialogAgain -> + if (shouldNotShowDialogAgain) { + nfcViewModel.setDoNotShowWrongCanDialog(true) + doNotShowWrongCanDialogAgain = true + } + showWrongCanDialog = false + }, + ) + } + Column( modifier = modifier diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt index ca4f9f520..15f7de843 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -37,7 +37,9 @@ import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository import ee.ria.DigiDoc.cryptolib.CDOC2Settings import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.domain.model.IdCardData +import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.domain.service.IdCardService +import ee.ria.DigiDoc.exceptions.NFCError import ee.ria.DigiDoc.idcard.CertificateType import ee.ria.DigiDoc.idcard.CodeType import ee.ria.DigiDoc.idcard.PaceTunnelException @@ -59,6 +61,9 @@ import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.ExternalSigner import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -76,6 +81,7 @@ class NFCViewModel private val cdoc2Settings: CDOC2Settings, private val configurationRepository: ConfigurationRepository, private val idCardService: IdCardService, + private val dataStore: DataStore, ) : ViewModel() { private val logTag = javaClass.simpleName @@ -84,8 +90,8 @@ class NFCViewModel private val _cryptoContainer = MutableLiveData(null) val cryptoContainer: LiveData = _cryptoContainer - private val _errorState = MutableLiveData?>(null) - val errorState: LiveData?> = _errorState + private val _errorState = MutableStateFlow(null) + val errorState: StateFlow = _errorState private val _message = MutableLiveData(null) val message: LiveData = _message @@ -115,7 +121,7 @@ class NFCViewModel ).build() fun resetErrorState() { - _errorState.postValue(null) + _errorState.value = null } fun resetSignStatus() { @@ -138,6 +144,12 @@ class NFCViewModel _shouldResetPIN.postValue(false) } + fun setDoNotShowWrongCanDialog(doNotShowAgain: Boolean) { + dataStore.setDoNotShowWrongCanDialog(doNotShowAgain) + } + + fun getDoNotShowWrongCanDialog(): Boolean = dataStore.getDoNotShowWrongCanDialog() + fun shouldShowCANNumberError(canNumber: String?): Boolean = ( !canNumber.isNullOrEmpty() && @@ -161,7 +173,7 @@ class NFCViewModel fun getNFCStatus(activity: Activity): NfcStatus = NfcStatus.NFC_ACTIVE private fun resetValues() { - _errorState.postValue(null) + _errorState.value = null _message.postValue(null) _signStatus.postValue(null) _decryptStatus.postValue(null) @@ -269,52 +281,49 @@ class NFCViewModel _signStatus.postValue(false) if (ex.message?.contains("TagLostException") == true) { - _errorState.postValue( - Triple( - R.string.signature_update_nfc_tag_lost, - null, - null, - ), - ) + _errorState.update { NFCError.TagLost(R.string.signature_update_nfc_tag_lost) } } else if (ex.message?.contains("PIN2 has not been changed") == true) { _dialogError.postValue(R.string.sign_blocked_pin2_unchanged_message) } else if (ex.message?.contains("PIN2 verification failed") == true && ex.message?.contains("Retries left: 2") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple( - R.string.id_card_sign_pin_invalid, + _errorState.update { + NFCError.WrongPin( pinType, 2, - ), - ) + R.string.id_card_sign_pin_invalid, + ) + } } else if (ex.message?.contains("PIN2 verification failed") == true && ex.message?.contains("Retries left: 1") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple( - R.string.id_card_sign_pin_invalid_final, + _errorState.update { + NFCError.WrongPin( pinType, - null, - ), - ) + 1, + R.string.id_card_sign_pin_invalid_final, + ) + } } else if (ex.message?.contains("PIN2 verification failed") == true && ex.message?.contains("Retries left: 0") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple(R.string.id_card_sign_pin_locked, pinType, null), - ) + _errorState.update { + NFCError.PinBlocked( + pinType, + R.string.id_card_sign_pin_locked, + ) + } } else if (ex is ApduResponseException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_technical_error, null, null), - ) + _errorState.update { + NFCError.ApduResponse( + R.string.signature_update_nfc_technical_error, + ) + } } else if (ex is PaceTunnelException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_wrong_can, null, null), - ) + _errorState.update { NFCError.WrongCan(R.string.signature_update_nfc_wrong_can) } } else { showTechnicalError(ex) } @@ -362,7 +371,7 @@ class NFCViewModel withContext(Main) { _nfcStatus.postValue(nfcSmartCardReaderManager.detectNfcStatus(activity)) _signStatus.postValue(false) - _errorState.postValue(Triple(R.string.error_general_client, null, null)) + _errorState.update { NFCError.WrongCan(R.string.error_general_client) } errorLog(logTag, "Unable to get container value. Container is 'null'") } } @@ -423,48 +432,47 @@ class NFCViewModel _decryptStatus.postValue(false) if (ex.message?.contains("TagLostException") == true) { - _errorState.postValue(Triple(R.string.signature_update_nfc_tag_lost, null, null)) + _errorState.update { NFCError.TagLost(R.string.signature_update_nfc_tag_lost) } } else if (ex.message?.contains("PIN1 verification failed") == true && ex.message?.contains("Retries left: 2") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple( - R.string.id_card_sign_pin_invalid, + _errorState.update { + NFCError.WrongPin( pinType, - 2, - ), - ) + R.string.id_card_sign_pin_invalid, + R.string.id_card_sign_pin_invalid, + ) + } } else if (ex.message?.contains("PIN1 verification failed") == true && ex.message?.contains("Retries left: 1") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple( - R.string.id_card_sign_pin_invalid_final, + _errorState.update { + NFCError.WrongPin( pinType, - null, - ), - ) + 1, + R.string.id_card_sign_pin_invalid, + ) + } } else if (ex.message?.contains("PIN1 verification failed") == true && ex.message?.contains("Retries left: 0") == true ) { _shouldResetPIN.postValue(true) - _errorState.postValue( - Triple( - R.string.id_card_sign_pin_locked, + _errorState.update { + NFCError.PinBlocked( pinType, - null, - ), - ) + R.string.id_card_sign_pin_locked, + ) + } } else if (ex is ApduResponseException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_technical_error, null, null), - ) + _errorState.update { + NFCError.ApduResponse( + R.string.signature_update_nfc_technical_error, + ) + } } else if (ex is PaceTunnelException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_wrong_can, null, null), - ) + _errorState.update { NFCError.WrongCan(R.string.signature_update_nfc_wrong_can) } } else { showTechnicalError(ex) } @@ -519,7 +527,7 @@ class NFCViewModel withContext(Main) { _nfcStatus.postValue(nfcSmartCardReaderManager.detectNfcStatus(activity)) _decryptStatus.postValue(false) - _errorState.postValue(Triple(R.string.error_general_client, null, null)) + _errorState.update { NFCError.GeneralError(R.string.error_general_client) } errorLog(logTag, "Unable to get container value. Container is 'null'") _shouldResetPIN.postValue(true) } @@ -554,21 +562,21 @@ class NFCViewModel resetIdCardUserData() if (e.message?.contains("TagLostException") == true) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_tag_lost, null, null), - ) + _errorState.update { NFCError.TagLost(R.string.signature_update_nfc_tag_lost) } } else if (e is ApduResponseException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_technical_error, null, null), - ) + _errorState.update { + NFCError.ApduResponse( + R.string.signature_update_nfc_technical_error, + ) + } } else if (e is PaceTunnelException) { - _errorState.postValue( - Triple(R.string.signature_update_nfc_wrong_can, null, null), - ) + _errorState.update { NFCError.WrongCan(R.string.signature_update_nfc_wrong_can) } } else { - _errorState.postValue( - Triple(R.string.signature_update_nfc_technical_error, null, null), - ) + _errorState.update { + NFCError.TechnicalError( + R.string.signature_update_nfc_technical_error, + ) + } } errorLog( @@ -609,49 +617,41 @@ class NFCViewModel ) { _dialogError.postValue(res) } else { - _errorState.postValue(res?.let { Triple(it, null, null) }) + _errorState.update { res?.let { NFCError.LimitExceeded(it) } } } } private fun showNetworkError(e: Exception) { - _errorState.postValue(Triple(R.string.no_internet_connection, null, null)) + _errorState.update { NFCError.NoInternetConnection(R.string.no_internet_connection) } errorLog(logTag, "Unable to sign with NFC - Unable to connect to Internet", e) } private fun showProxyError(e: Exception) { - _errorState.postValue(Triple(R.string.main_settings_proxy_invalid_settings, null, null)) + _errorState.update { NFCError.NoProxyConnection(R.string.main_settings_proxy_invalid_settings) } errorLog(logTag, "Unable to sign with NFC - Unable to create proxy connection with host", e) } private fun showNoLockFoundError(e: Exception) { - _errorState.postValue(Triple(R.string.no_lock_found, null, null)) + _errorState.update { NFCError.NoLockFound(R.string.no_lock_found) } errorLog(logTag, "Unable to decrypt with NFC - No lock found with certificate key", e) } private fun showRevokedCertificateError(e: Exception) { - _errorState.postValue( - Triple( - R.string.signature_update_signature_error_message_certificate_revoked, - null, - null, - ), - ) + _errorState.update { + NFCError.CertificateRevoked(R.string.signature_update_signature_error_message_certificate_revoked) + } errorLog(logTag, "Unable to sign with NFC - Certificate status: revoked", e) } private fun showUnknownCertificateError(e: Exception) { - _errorState.postValue( - Triple( - R.string.signature_update_signature_error_message_certificate_unknown, - null, - null, - ), - ) + _errorState.update { + NFCError.CertificateUnknown(R.string.signature_update_signature_error_message_certificate_unknown) + } errorLog(logTag, "Unable to sign with NFC - Certificate status: unknown", e) } private fun showTechnicalError(e: Exception) { - _errorState.postValue(Triple(R.string.signature_update_nfc_technical_error, null, null)) + _errorState.update { NFCError.TechnicalError(R.string.signature_update_nfc_technical_error) } errorLog(logTag, "Unable to perform with NFC: ${e.message}", e) } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt index 6c21e2f2c..bbc187b21 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt @@ -145,6 +145,8 @@ class SharedSettingsViewModel resetCertificateInfo() resetErrors() + + resetShowingWrongCanNumberDialog() } private fun resetProxySettings() { @@ -157,6 +159,10 @@ class SharedSettingsViewModel setManualProxySettings(manualProxySettings) } + private fun resetShowingWrongCanNumberDialog() { + dataStore.setDoNotShowWrongCanDialog(false) + } + private fun setManualProxySettings(manualProxy: ManualProxy) { dataStore.setProxyHost(manualProxy.host) dataStore.setProxyPort(manualProxy.port) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0164a1153..6cc388a47 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -1,6 +1,10 @@ versioon %1$s + Loe täpsemalt siit + Ära rohkem näita + Dialoog + Failid Leia vajalik fail või dokument @@ -336,6 +340,8 @@ Hoia ID-kaarti telefoni lähedal Vale CAN number ID-kaardiga ühenduse loomine + NFC abil tehtud toimingutes kasutatakse CAN numbrit. Mitmekordsel CANi valesti sisestamisel võib NFC lukustuda. + https://www.id.ee/artikkel/digiallkirjastamine-ria-digidoc-mobiilirakenduses/ Allkirjasta Edasi @@ -526,7 +532,6 @@ Salvesta logifail Logimise aktiveerimiseks taaskäivita RIA DigiDoc. https://www.id.ee/artikkel/logifaili-genereerimine-digidoc4-kliendis/ - Loe täpsemalt siit Taaskäivita rakendus? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37d811938..9c40a73e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,10 @@ version %1$s + Read more here + Don\'t show again + Dialog + Files Find the file or document you need @@ -336,6 +340,8 @@ Wrong CAN number Hold your phone near the ID-card Authenticating with ID-card + NFC actions use the CAN number. NFC may lock up if CAN is entered incorrectly multiple times. + https://www.id.ee/en/article/digital-signing-in-ria-digidoc-mobile-application/ Sign Next @@ -526,7 +532,6 @@ Save log Restart RIA DigiDoc to activate logging. https://www.id.ee/en/article/log-file-generation-in-digidoc4-client/ - Read more here Restart now? diff --git a/commons-lib/src/main/kotlin/ee/ria/DigiDoc/common/Constant.kt b/commons-lib/src/main/kotlin/ee/ria/DigiDoc/common/Constant.kt index 8e556eea7..5a2c5b242 100644 --- a/commons-lib/src/main/kotlin/ee/ria/DigiDoc/common/Constant.kt +++ b/commons-lib/src/main/kotlin/ee/ria/DigiDoc/common/Constant.kt @@ -56,6 +56,8 @@ object Constant { const val PIN_MAX_LENGTH: Int = 12 const val PUK_MIN_LENGTH: Int = 8 const val CAN_LENGTH: Int = 6 + + const val DO_NOT_SHOW_WRONG_CAN_DIALOG: String = "DO_NOT_SHOW_WRONG_CAN_DIALOG" } object SmartIdConstants {