PIN unlock : makes sure to load the pin size from storage
This commit is contained in:
@@ -21,7 +21,7 @@ object LockScreenConfig {
|
||||
/**
|
||||
* Whether the PIN is mandatory or not.
|
||||
*/
|
||||
const val IS_PIN_MANDATORY: Boolean = true
|
||||
const val IS_PIN_MANDATORY: Boolean = false
|
||||
|
||||
/**
|
||||
* Some PINs are blacklisted.
|
||||
|
||||
@@ -20,7 +20,8 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
@@ -58,16 +59,17 @@ fun PinEntryTextField(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun PinEntryRow(
|
||||
pinEntry: PinEntry,
|
||||
isSecured: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
for (digit in pinEntry.digits) {
|
||||
PinDigitView(digit = digit, isSecured = isSecured)
|
||||
@@ -98,7 +100,7 @@ private fun PinDigitView(
|
||||
|
||||
) {
|
||||
if (digit is PinDigit.Filled) {
|
||||
val text = if(isSecured) {
|
||||
val text = if (isSecured) {
|
||||
"•"
|
||||
} else {
|
||||
digit.value.toString()
|
||||
|
||||
@@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult
|
||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor(
|
||||
return pinCodeStore.hasPinCode()
|
||||
}
|
||||
|
||||
override suspend fun getPinCodeSize(): Int {
|
||||
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0
|
||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
||||
return decryptedPinCode.size
|
||||
}
|
||||
|
||||
override suspend fun createPinCode(pinCode: String) {
|
||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
||||
|
||||
@@ -57,6 +57,11 @@ interface PinCodeManager {
|
||||
*/
|
||||
suspend fun isPinCodeAvailable(): Boolean
|
||||
|
||||
/**
|
||||
* @return the size of the saved pin code.
|
||||
*/
|
||||
suspend fun getPinCodeSize(): Int
|
||||
|
||||
/**
|
||||
* Creates a new encrypted pin code.
|
||||
* @param pinCode the clear pin code to create
|
||||
|
||||
@@ -24,13 +24,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.appconfig.LockScreenConfig
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor(
|
||||
|
||||
@Composable
|
||||
override fun present(): PinUnlockState {
|
||||
var pinEntry by remember {
|
||||
//TODO fetch size from db
|
||||
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
|
||||
val pinEntryState = remember {
|
||||
mutableStateOf<Async<PinEntry>>(Async.Uninitialized)
|
||||
}
|
||||
val pinEntry by pinEntryState
|
||||
var remainingAttempts by remember {
|
||||
mutableStateOf<Async<Int>>(Async.Uninitialized)
|
||||
}
|
||||
@@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor(
|
||||
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
suspend {
|
||||
val pinCodeSize = pinCodeManager.getPinCodeSize()
|
||||
PinEntry.createEmpty(pinCodeSize)
|
||||
}.runCatchingUpdatingState(pinEntryState)
|
||||
}
|
||||
|
||||
LaunchedEffect(pinEntry) {
|
||||
if (pinEntry.isComplete()) {
|
||||
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
|
||||
if (!isVerified) {
|
||||
pinEntry = pinEntry.clear()
|
||||
pinEntryState.value = pinEntry.clear()
|
||||
showWrongPinTitle = true
|
||||
}
|
||||
}
|
||||
@@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||
fun handleEvents(event: PinUnlockEvents) {
|
||||
when (event) {
|
||||
is PinUnlockEvents.OnPinKeypadPressed -> {
|
||||
pinEntry = pinEntry.process(event.pinKeypadModel)
|
||||
pinEntryState.value = pinEntry.process(event.pinKeypadModel)
|
||||
}
|
||||
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
||||
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
||||
@@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.isComplete(): Boolean {
|
||||
return dataOrNull()?.isComplete().orFalse()
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.toText(): String {
|
||||
return dataOrNull()?.toText() ?: ""
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.clear(): Async<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> Async.Success(data.clear())
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> {
|
||||
val pinEntry = when (pinKeypadModel) {
|
||||
PinKeypadModel.Back -> data.deleteLast()
|
||||
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
|
||||
PinKeypadModel.Empty -> data
|
||||
}
|
||||
Async.Success(pinEntry)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
|
||||
suspend {
|
||||
matrixClient.logout()
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
}
|
||||
|
||||
private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry {
|
||||
return when (pinKeypadModel) {
|
||||
PinKeypadModel.Back -> deleteLast()
|
||||
is PinKeypadModel.Number -> addDigit(pinKeypadModel.number)
|
||||
PinKeypadModel.Empty -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class PinUnlockState(
|
||||
val pinEntry: PinEntry,
|
||||
val pinEntry: Async<PinEntry>,
|
||||
val showWrongPinTitle: Boolean,
|
||||
val remainingAttempts: Async<Int>,
|
||||
val showSignOutPrompt: Boolean,
|
||||
|
||||
@@ -39,7 +39,7 @@ fun aPinUnlockState(
|
||||
showSignOutPrompt: Boolean = false,
|
||||
signOutAction: Async<String?> = Async.Uninitialized,
|
||||
) = PinUnlockState(
|
||||
pinEntry = pinEntry,
|
||||
pinEntry = Async.Success(pinEntry),
|
||||
showWrongPinTitle = showWrongPinTitle,
|
||||
remainingAttempts = Async.Success(remainingAttempts),
|
||||
showSignOutPrompt = showSignOutPrompt,
|
||||
|
||||
@@ -258,7 +258,7 @@ private fun PinUnlockHeader(
|
||||
if (state.showWrongPinTitle) {
|
||||
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
|
||||
} else {
|
||||
stringResource(id = R.string.screen_app_lock_subtitle)
|
||||
pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts)
|
||||
}
|
||||
} else {
|
||||
""
|
||||
@@ -276,14 +276,16 @@ private fun PinUnlockHeader(
|
||||
color = subtitleColor,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
PinDotsRow(state.pinEntry)
|
||||
if (state.pinEntry is Async.Success) {
|
||||
PinDotsRow(state.pinEntry.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinUnlockFooter(
|
||||
onUseBiometric: ()->Unit,
|
||||
onForgotPin: ()->Unit,
|
||||
onUseBiometric: () -> Unit,
|
||||
onForgotPin: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="one">"You have %1$d attempt to unlock"</item>
|
||||
<item quantity="other">"You have %1$d attempts to unlock"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"Wrong PIN. You have %1$d more chance"</item>
|
||||
<item quantity="other">"Wrong PIN. You have %1$d more chances"</item>
|
||||
@@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"You’ll need to re-login and create a new PIN to proceed"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
||||
<string name="screen_app_lock_subtitle">"You have 3 attempts to unlock"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user