Skip to content

Commit b460d28

Browse files
authored
Merge pull request #10031 from wmontwe/fix/10013/password-prompt-not-shown
fix: biometric prompt is not shown
2 parents 89f86b8 + f44dad5 commit b460d28

File tree

9 files changed

+461
-154
lines changed

9 files changed

+461
-154
lines changed

feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/common/BiometricPasswordInput.kt renamed to feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/common/ProtectedPasswordInput.kt

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package app.k9mail.feature.account.server.settings.ui.common
22

3-
import androidx.biometric.BiometricPrompt
3+
import androidx.annotation.StringRes
44
import androidx.compose.foundation.layout.PaddingValues
55
import androidx.compose.foundation.layout.fillMaxWidth
66
import androidx.compose.runtime.Composable
@@ -11,62 +11,71 @@ import androidx.compose.runtime.remember
1111
import androidx.compose.runtime.setValue
1212
import androidx.compose.ui.Modifier
1313
import androidx.compose.ui.res.stringResource
14-
import androidx.fragment.app.FragmentActivity
1514
import app.k9mail.core.ui.compose.designsystem.molecule.input.InputLayout
16-
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
1715
import app.k9mail.core.ui.compose.designsystem.molecule.input.inputContentPadding
1816
import app.k9mail.feature.account.server.settings.R
1917
import kotlinx.coroutines.delay
18+
import net.thunderbird.feature.account.server.settings.ui.common.AuthenticationError
19+
import net.thunderbird.feature.account.server.settings.ui.common.Authenticator
20+
import net.thunderbird.feature.account.server.settings.ui.common.rememberBiometricAuthenticator
2021
import app.k9mail.core.ui.compose.designsystem.R as RDesign
2122

2223
private const val SHOW_WARNING_DURATION = 5000L
2324

2425
/**
2526
* Variant of [PasswordInput] that only allows the password to be unmasked after the user has authenticated using
26-
* [BiometricPrompt].
27-
*
28-
* Note: Due to limitations of [BiometricPrompt] this composable can only be used inside a [FragmentActivity].
27+
* [Authenticator] that defaults to [rememberBiometricAuthenticator].
2928
*/
3029
@Composable
31-
fun BiometricPasswordInput(
30+
fun ProtectedPasswordInput(
3231
onPasswordChange: (String) -> Unit,
3332
modifier: Modifier = Modifier,
3433
password: String = "",
3534
isRequired: Boolean = false,
3635
errorMessage: String? = null,
3736
contentPadding: PaddingValues = inputContentPadding(),
37+
authenticator: Authenticator = rememberBiometricAuthenticator(
38+
title = stringResource(R.string.account_server_settings_password_authentication_title),
39+
subtitle = stringResource(R.string.account_server_settings_password_authentication_subtitle),
40+
),
3841
) {
39-
var biometricWarning by remember { mutableStateOf<String?>(value = null) }
42+
var authenticationError by remember { mutableStateOf<AuthenticationError?>(value = null) }
43+
val authenticationWarning = authenticationError?.let { stringResource(it.mapToStringRes()) }
4044

41-
LaunchedEffect(key1 = biometricWarning) {
42-
if (biometricWarning != null) {
45+
LaunchedEffect(key1 = authenticationError) {
46+
if (authenticationError != null) {
4347
delay(SHOW_WARNING_DURATION)
44-
biometricWarning = null
48+
authenticationError = null
4549
}
4650
}
4751

4852
InputLayout(
4953
modifier = modifier,
5054
contentPadding = contentPadding,
5155
errorMessage = errorMessage,
52-
warningMessage = biometricWarning,
56+
warningMessage = authenticationWarning,
5357
) {
54-
val title = stringResource(R.string.account_server_settings_password_authentication_title)
55-
val subtitle = stringResource(R.string.account_server_settings_password_authentication_subtitle)
56-
val needScreenLockMessage =
57-
stringResource(R.string.account_server_settings_password_authentication_screen_lock_required)
58-
59-
TextFieldOutlinedPasswordBiometric(
58+
ProtectedTextFieldOutlinedPassword(
6059
value = password,
6160
onValueChange = onPasswordChange,
62-
authenticationTitle = title,
63-
authenticationSubtitle = subtitle,
64-
needScreenLockMessage = needScreenLockMessage,
65-
onWarningChange = { biometricWarning = it?.toString() },
61+
onWarningChange = { authenticationError = it },
62+
authenticator = authenticator,
6663
label = stringResource(id = RDesign.string.designsystem_molecule_password_input_label),
6764
isRequired = isRequired,
6865
hasError = errorMessage != null,
6966
modifier = Modifier.fillMaxWidth(),
7067
)
7168
}
7269
}
70+
71+
@StringRes
72+
private fun AuthenticationError.mapToStringRes(): Int {
73+
return when (this) {
74+
AuthenticationError.NotAvailable ->
75+
R.string.account_server_settings_password_authentication_screen_lock_required
76+
AuthenticationError.Failed ->
77+
R.string.account_server_settings_password_authentication_failed
78+
AuthenticationError.UnableToStart ->
79+
R.string.account_server_settings_password_authentication_unable_to_start
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package app.k9mail.feature.account.server.settings.ui.common
2+
3+
import android.app.Activity
4+
import android.view.WindowManager
5+
import androidx.activity.compose.LocalActivity
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.DisposableEffect
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.rememberCoroutineScope
11+
import androidx.compose.runtime.saveable.rememberSaveable
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.Modifier
14+
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedPassword
15+
import kotlinx.coroutines.launch
16+
import net.thunderbird.core.outcome.Outcome
17+
import net.thunderbird.feature.account.server.settings.ui.common.AuthenticationError
18+
import net.thunderbird.feature.account.server.settings.ui.common.Authenticator
19+
20+
/**
21+
* Variant of [TextFieldOutlinedPassword] that only allows the
22+
* password to be unmasked after the user has authenticated using [Authenticator].
23+
*/
24+
@Suppress("LongParameterList")
25+
@Composable
26+
fun ProtectedTextFieldOutlinedPassword(
27+
value: String,
28+
onValueChange: (String) -> Unit,
29+
onWarningChange: (AuthenticationError?) -> Unit,
30+
authenticator: Authenticator,
31+
modifier: Modifier = Modifier,
32+
label: String? = null,
33+
isEnabled: Boolean = true,
34+
isReadOnly: Boolean = false,
35+
isRequired: Boolean = false,
36+
hasError: Boolean = false,
37+
) {
38+
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
39+
var isAuthenticated by rememberSaveable { mutableStateOf(false) }
40+
41+
val activity = LocalActivity.current as Activity
42+
val scope = rememberCoroutineScope()
43+
44+
TextFieldOutlinedPassword(
45+
value = value,
46+
onValueChange = onValueChange,
47+
modifier = modifier,
48+
label = label,
49+
isEnabled = isEnabled,
50+
isReadOnly = isReadOnly,
51+
isRequired = isRequired,
52+
hasError = hasError,
53+
isPasswordVisible = isPasswordVisible,
54+
onPasswordVisibilityToggleClicked = {
55+
if (isAuthenticated) {
56+
isPasswordVisible = !isPasswordVisible
57+
activity.setSecure(isPasswordVisible)
58+
} else {
59+
scope.launch {
60+
when (val outcome = authenticator.authenticate()) {
61+
is Outcome.Success -> {
62+
isAuthenticated = true
63+
isPasswordVisible = true
64+
onWarningChange(null)
65+
activity.setSecure(true)
66+
}
67+
is Outcome.Failure -> {
68+
onWarningChange(outcome.error)
69+
}
70+
}
71+
}
72+
}
73+
},
74+
)
75+
76+
DisposableEffect(key1 = "secureWindow") {
77+
activity.setSecure(isPasswordVisible)
78+
79+
onDispose {
80+
activity.setSecure(false)
81+
}
82+
}
83+
}
84+
85+
private fun Activity.setSecure(secure: Boolean) {
86+
window.setFlags(if (secure) WindowManager.LayoutParams.FLAG_SECURE else 0, WindowManager.LayoutParams.FLAG_SECURE)
87+
}

feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/common/ServerSettingsPasswordInput.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fun ServerSettingsPasswordInput(
2727
contentPadding = contentPadding,
2828
)
2929
} else {
30-
BiometricPasswordInput(
30+
ProtectedPasswordInput(
3131
onPasswordChange = onPasswordChange,
3232
modifier = modifier,
3333
password = password,

feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/common/TextFieldOutlinedPasswordBiometric.kt

Lines changed: 0 additions & 131 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package net.thunderbird.feature.account.server.settings.ui.common
2+
3+
import net.thunderbird.core.outcome.Outcome
4+
5+
/**
6+
* A functional interface for authenticating a user.
7+
*/
8+
fun interface Authenticator {
9+
10+
/**
11+
* Authenticates the user.
12+
*
13+
* @return An [Outcome] representing the result of the authentication process.
14+
*/
15+
suspend fun authenticate(): Outcome<Unit, AuthenticationError>
16+
}
17+
18+
/**
19+
* Authentication errors that can occur during the authentication process.
20+
*/
21+
sealed interface AuthenticationError {
22+
/**
23+
* The user has not set up any authentication methods (e.g. screen lock, biometrics).
24+
*/
25+
data object NotAvailable : AuthenticationError
26+
27+
/**
28+
* The authentication failed.
29+
*/
30+
data object Failed : AuthenticationError
31+
32+
/**
33+
* An unknown error occurred, and authentication could not be started.
34+
*/
35+
data object UnableToStart : AuthenticationError
36+
}

0 commit comments

Comments
 (0)