From 97cd5f6c3f5c38e3b45e2cc08e74dd7caa31409c Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 6 Nov 2026 14:34:51 +0200 Subject: [PATCH] Show expired certificate status in My eID --- .../DigiDoc/ui/component/myeid/MyEidScreen.kt | 76 ++++++++++++++++--- .../myeid/mydata/MyEidDocumentStatus.kt | 17 +++++ .../MyEidPinAndCertificateView.kt | 44 +++++++++-- app/src/main/res/values-et/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- .../myeid/mydata/MyEidDocumentStatusTest.kt | 44 +++++++++++ 6 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 app/src/test/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatusTest.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt index 532a8fb8a..1f9eefadb 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -52,7 +53,9 @@ import ee.ria.DigiDoc.idcard.CardType import ee.ria.DigiDoc.idcard.CodeType import ee.ria.DigiDoc.idcard.DateOfBirthUtil import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet +import ee.ria.DigiDoc.ui.component.myeid.mydata.MyEidDocumentStatus import ee.ria.DigiDoc.ui.component.myeid.mydata.MyEidMyDataView +import ee.ria.DigiDoc.ui.component.myeid.mydata.certificateStatus import ee.ria.DigiDoc.ui.component.myeid.pinandcertificate.MyEidPinAndCertificateView import ee.ria.DigiDoc.ui.component.shared.HrefDynamicText import ee.ria.DigiDoc.ui.component.shared.StatusSnackbarHost @@ -90,6 +93,11 @@ fun MyEidScreen( val isPin2Activated = idCardData?.pin2CodeChanged == true val isCourierCard = idCardData?.personalData != null && idCardData?.pin1CodeChanged == false + val authCertificate = remember(idCardData) { idCardData?.authCertificate?.data?.x509Certificate() } + val signCertificate = remember(idCardData) { idCardData?.signCertificate?.data?.x509Certificate() } + val authCertificateStatus = idCardData?.let { certificateStatus(authCertificate) } + val signCertificateStatus = idCardData?.let { certificateStatus(signCertificate) } + val alphaForPin1BlockedState = getAlphaForBlockedState((isPin1Blocked && isPukBlocked) || isCourierCard) val alphaForPin2BlockedState = getAlphaForBlockedState((isPin2Blocked && isPukBlocked) || isCourierCard) val alphaForPukBlockedState = getAlphaForBlockedState(isPukBlocked) @@ -310,15 +318,14 @@ fun MyEidScreen( .alpha(alphaForPin1BlockedState), title = stringResource(R.string.myeid_authentication_certificate_title), subtitle = - stringResource( - R.string.myeid_certificate_valid_to, - sharedMyEidViewModel.getNotAfter( - idCardData?.authCertificate?.data?.x509Certificate(), - ), + certificateSubtitle( + authCertificateStatus, + sharedMyEidViewModel.getNotAfter(authCertificate), ), isPinBlocked = isPin1Blocked, isPukBlocked = isPukBlocked, isNotActivated = isCourierCard, + certificateStatus = authCertificateStatus, forgotPinText = if (isPin1Blocked) { stringResource( @@ -344,6 +351,14 @@ fun MyEidScreen( }, ) + if (authCertificateStatus == MyEidDocumentStatus.EXPIRED) { + ExpiredCertificateText( + modifier = modifier, + testTag = "myEidExpiredPin1DescriptionText", + codeType = CodeType.PIN1, + ) + } + if (isPin1Blocked && !isPukBlocked) { Text( modifier = @@ -398,15 +413,14 @@ fun MyEidScreen( .alpha(alphaForPin2BlockedState), title = stringResource(R.string.myeid_signing_certificate_title), subtitle = - stringResource( - R.string.myeid_certificate_valid_to, - sharedMyEidViewModel.getNotAfter( - idCardData?.signCertificate?.data?.x509Certificate(), - ), + certificateSubtitle( + signCertificateStatus, + sharedMyEidViewModel.getNotAfter(signCertificate), ), isPinBlocked = isPin2Blocked, isPukBlocked = isPukBlocked, isNotActivated = isCourierCard, + certificateStatus = signCertificateStatus, forgotPinText = if (isPin2Blocked) { stringResource( @@ -432,12 +446,22 @@ fun MyEidScreen( }, ) + if (signCertificateStatus == MyEidDocumentStatus.EXPIRED) { + ExpiredCertificateText( + modifier = modifier, + testTag = "myEidExpiredPin2DescriptionText", + codeType = CodeType.PIN2, + ) + } + if (isCourierCard) { CourierCardWarningText( modifier = modifier, testTag = "myEidCourierCardPin2DescriptionText", ) - } else if (!isPin2Activated) { + } else if (!isPin2Activated && + signCertificateStatus != MyEidDocumentStatus.EXPIRED + ) { Text( modifier = modifier @@ -623,6 +647,36 @@ fun MyEidScreen( ) } +@Composable +private fun certificateSubtitle( + status: MyEidDocumentStatus?, + validTo: String, +): String = + when (status) { + MyEidDocumentStatus.EXPIRED -> + stringResource(R.string.signature_update_signature_error_message_certificate_expired) + MyEidDocumentStatus.UNKNOWN -> stringResource(R.string.myeid_status_unknown) + else -> stringResource(R.string.myeid_certificate_valid_to, validTo) + } + +@Composable +private fun ExpiredCertificateText( + modifier: Modifier, + testTag: String, + codeType: CodeType, +) { + Text( + modifier = + modifier + .fillMaxWidth() + .focusable(true) + .testTag(testTag), + text = stringResource(R.string.myeid_cert_expired, codeType), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) +} + @Composable private fun CourierCardWarningText( modifier: Modifier, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatus.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatus.kt index f6406313b..463d0c926 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatus.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatus.kt @@ -7,6 +7,9 @@ package ee.ria.DigiDoc.ui.component.myeid.mydata import android.content.Context import ee.ria.DigiDoc.R +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import java.security.cert.X509Certificate enum class MyEidDocumentStatus { VALID, @@ -20,3 +23,17 @@ fun MyEidDocumentStatus.getLocalized(context: Context): String = MyEidDocumentStatus.EXPIRED -> context.getString(R.string.myeid_status_expired) MyEidDocumentStatus.UNKNOWN -> context.getString(R.string.myeid_status_unknown) } + +fun certificateStatus(x509Certificate: X509Certificate?): MyEidDocumentStatus { + if (x509Certificate == null) { + return MyEidDocumentStatus.UNKNOWN + } + return try { + x509Certificate.checkValidity() + MyEidDocumentStatus.VALID + } catch (cee: CertificateExpiredException) { + MyEidDocumentStatus.EXPIRED + } catch (cnyve: CertificateNotYetValidException) { + MyEidDocumentStatus.UNKNOWN + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt index 05c2514e6..8dd341ae5 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.myeid.mydata.MyEidDocumentStatus import ee.ria.DigiDoc.ui.component.shared.HrefDynamicText import ee.ria.DigiDoc.ui.theme.Dimensions.LINE_HEIGHT import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -64,6 +65,7 @@ fun MyEidPinAndCertificateView( isPinBlocked: Boolean = false, isPukBlocked: Boolean = false, isNotActivated: Boolean = false, + certificateStatus: MyEidDocumentStatus? = null, showForgotPin: Boolean = true, forgotPinText: String = "", onForgotPinClick: (() -> Unit)? = null, @@ -72,6 +74,15 @@ fun MyEidPinAndCertificateView( ) { val spokenTitleAndSubtitle = spokenDates("$title. $subtitle").lowercase() + val isCertificateUsable = certificateStatus == null || certificateStatus == MyEidDocumentStatus.VALID + val showErrorIcon = !isCertificateUsable || isPinBlocked + val subtitleColor = + if (isCertificateUsable) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.error + } + Column( modifier = modifier @@ -101,9 +112,21 @@ fun MyEidPinAndCertificateView( horizontalArrangement = Arrangement.Start, ) { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_m3_check_circle_48dp_wght400), + imageVector = + ImageVector.vectorResource( + if (showErrorIcon) { + R.drawable.ic_m3_error_48dp_wght400 + } else { + R.drawable.ic_m3_check_circle_48dp_wght400 + }, + ), contentDescription = null, - tint = MaterialTheme.colorScheme.primary, + tint = + if (showErrorIcon) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, modifier = modifier .size(iconSizeM) @@ -141,7 +164,7 @@ fun MyEidPinAndCertificateView( newLineBeforeLink = true, textStyle = TextStyle( - color = MaterialTheme.colorScheme.onSurface, + color = subtitleColor, fontSize = MaterialTheme.typography.bodyMedium.fontSize, textAlign = TextAlign.Start, lineHeight = TextUnit(LINE_HEIGHT, TextUnitType.Sp), @@ -152,7 +175,7 @@ fun MyEidPinAndCertificateView( modifier = modifier.notAccessible(), text = subtitle, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, + color = subtitleColor, ) } } @@ -241,6 +264,15 @@ fun MyEidPinAndCertificateViewPreview() { MyEidPinAndCertificateView( title = "Identity certificate", subtitle = "Certificate is valid until ${LocalDate.now()}", + certificateStatus = MyEidDocumentStatus.VALID, + forgotPinText = "Forgot PIN?", + onForgotPinClick = {}, + changePinText = "Change PIN", + ) + MyEidPinAndCertificateView( + title = "Signing certificate", + subtitle = "Certificate has expired", + certificateStatus = MyEidDocumentStatus.EXPIRED, forgotPinText = "Forgot PIN?", onForgotPinClick = {}, changePinText = "Change PIN", @@ -248,7 +280,9 @@ fun MyEidPinAndCertificateViewPreview() { MyEidPinAndCertificateView( title = "Identity certificate", subtitle = "Certificate is valid until ${LocalDate.now()}", - forgotPinText = "Forgot PIN?", + isPinBlocked = true, + certificateStatus = MyEidDocumentStatus.VALID, + forgotPinText = "Unblock PIN", onForgotPinClick = {}, changePinText = "Change PIN", ) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index de87b963f..48482de5e 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -706,7 +706,7 @@ https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/ PUK on blokeeritud, kuna PUK-koodi on sisestatud 3 korda valesti. PUK-koodi ei saa ise lahti blokeerida.\n\nKuigi PUK-kood on blokeeritud, saab kõiki eID võimalusi kasutada, välja arvatud PUK-koodi vajavaid.\n\nUue PUK-koodi saamiseks tuleb taotleda uus dokument. https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/ - Sertifikaat on <c>aegunud</c>! %1$s ei saa kasutada, kuna sertifikaat on aegunud. + %1$s ei saa kasutada, kuna sertifikaat on aegunud. Uus %1$s-kood ei saa olla osa sinu isikukoodist Uus %1$s-kood ei saa olla sinu sünnikuupäev Uus %1$s-kood ei saa olla nii lihtne diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index edaed2d5b..0fba25de5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -706,7 +706,7 @@ https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/ PUK has been blocked because PUK code has been entered incorrectly 3 times. You can not unblock the PUK code yourself.\n\nAs long as the PUK code is blocked, all eID options can be used, except transactions that need PUK code.\n\nA new document must be requested to receive the new PUK code. https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/ - Certificate has <c>expired</c>! %1$s cannot be used because the certificate has expired. + %1$s cannot be used because the certificate has expired. New %1$s code can\'t be part of your personal code New %1$s code can\'t be your date of birth New %1$s code can\'t be so easy diff --git a/app/src/test/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatusTest.kt b/app/src/test/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatusTest.kt new file mode 100644 index 000000000..5cd9c53d9 --- /dev/null +++ b/app/src/test/kotlin/ee/ria/DigiDoc/ui/component/myeid/mydata/MyEidDocumentStatusTest.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.ui.component.myeid.mydata + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import java.security.cert.X509Certificate + +class MyEidDocumentStatusTest { + @Test + fun myEidDocumentStatus_certificateStatus_returnsUnknownWhenCertificateIsNull() { + assertEquals(MyEidDocumentStatus.UNKNOWN, certificateStatus(null)) + } + + @Test + fun myEidDocumentStatus_certificateStatus_returnsExpiredWhenCertificateHasExpired() { + val certificate = mock() + whenever(certificate.checkValidity()).thenThrow(CertificateExpiredException()) + + assertEquals(MyEidDocumentStatus.EXPIRED, certificateStatus(certificate)) + } + + @Test + fun myEidDocumentStatus_certificateStatus_returnsUnknownWhenCertificateIsNotYetValid() { + val certificate = mock() + whenever(certificate.checkValidity()).thenThrow(CertificateNotYetValidException()) + + assertEquals(MyEidDocumentStatus.UNKNOWN, certificateStatus(certificate)) + } + + @Test + fun myEidDocumentStatus_certificateStatus_returnsValidWhenCertificateIsWithinItsValidityPeriod() { + val certificate = mock() + + assertEquals(MyEidDocumentStatus.VALID, certificateStatus(certificate)) + } +}