Pin: add LockScreenConfig and address PR reviews

This commit is contained in:
ganfra 2023-10-23 15:30:17 +02:00
parent bf8cf00c51
commit d21623e523
15 changed files with 71 additions and 38 deletions

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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 io.element.android.appconfig
object LockScreenConfig {
/**
* Whether the LockScreen is mandatory or not.
*/
const val IS_MANDATORY: Boolean = false
/**
* Some PINs are blacklisted.
*/
val PIN_BLACKLIST = listOf("0000", "1234")
/**
* The size of the PIN.
*/
const val PIN_SIZE = 4
}

View file

@ -30,9 +30,11 @@ anvil {
} }
dependencies { dependencies {
ksp(libs.showkase.processor)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
api(projects.features.lockscreen.api) api(projects.features.lockscreen.api)
implementation(projects.appconfig)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
@ -52,7 +54,4 @@ dependencies {
testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.test)
testImplementation(projects.libraries.cryptography.impl) testImplementation(projects.libraries.cryptography.impl)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
ksp(libs.showkase.processor)
} }

View file

@ -110,7 +110,7 @@ private fun PinDigitView(
internal fun PinEntryTextFieldPreview() { internal fun PinEntryTextFieldPreview() {
ElementPreview { ElementPreview {
PinEntryTextField( PinEntryTextField(
pinEntry = PinEntry.empty(4).fillWith("12"), pinEntry = PinEntry.createEmpty(4).fillWith("12"),
onValueChange = {}, onValueChange = {},
) )
} }

View file

@ -24,7 +24,7 @@ data class PinEntry(
) { ) {
companion object { companion object {
fun empty(size: Int): PinEntry { fun createEmpty(size: Int): PinEntry {
val digits = List(size) { PinDigit.Empty } val digits = List(size) { PinDigit.Empty }
return PinEntry( return PinEntry(
digits = digits.toPersistentList() digits = digits.toPersistentList()
@ -69,7 +69,7 @@ data class PinEntry(
} }
fun clear(): PinEntry { fun clear(): PinEntry {
return fillWith("") return createEmpty(size)
} }
fun isComplete(): Boolean { fun isComplete(): Boolean {

View file

@ -21,15 +21,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import javax.inject.Inject import javax.inject.Inject
private const val PIN_SIZE = 4
class SetupPinPresenter @Inject constructor( class SetupPinPresenter @Inject constructor(
private val pinValidator: PinValidator, private val pinValidator: PinValidator,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
@ -38,10 +37,10 @@ class SetupPinPresenter @Inject constructor(
@Composable @Composable
override fun present(): SetupPinState { override fun present(): SetupPinState {
var choosePinEntry by remember { var choosePinEntry by remember {
mutableStateOf(PinEntry.empty(PIN_SIZE)) mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
} }
var confirmPinEntry by remember { var confirmPinEntry by remember {
mutableStateOf(PinEntry.empty(PIN_SIZE)) mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
} }
var isConfirmationStep by remember { var isConfirmationStep by remember {
mutableStateOf(false) mutableStateOf(false)
@ -77,11 +76,11 @@ class SetupPinPresenter @Inject constructor(
SetupPinEvents.ClearFailure -> { SetupPinEvents.ClearFailure -> {
when (setupPinFailure) { when (setupPinFailure) {
is SetupPinFailure.PinsDontMatch -> { is SetupPinFailure.PinsDontMatch -> {
choosePinEntry = PinEntry.empty(PIN_SIZE) choosePinEntry = choosePinEntry.clear()
confirmPinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = confirmPinEntry.clear()
} }
is SetupPinFailure.PinBlacklisted -> { is SetupPinFailure.PinBlacklisted -> {
choosePinEntry = PinEntry.empty(PIN_SIZE) choosePinEntry = choosePinEntry.clear()
} }
null -> Unit null -> Unit
} }

View file

@ -25,20 +25,20 @@ open class SetupPinStateProvider : PreviewParameterProvider<SetupPinState> {
get() = sequenceOf( get() = sequenceOf(
aSetupPinState(), aSetupPinState(),
aSetupPinState( aSetupPinState(
choosePinEntry = PinEntry.empty(4).fillWith("12") choosePinEntry = PinEntry.createEmpty(4).fillWith("12")
), ),
aSetupPinState( aSetupPinState(
choosePinEntry = PinEntry.empty(4).fillWith("1789"), choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"),
isConfirmationStep = true, isConfirmationStep = true,
), ),
aSetupPinState( aSetupPinState(
choosePinEntry = PinEntry.empty(4).fillWith("1789"), choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"),
confirmPinEntry = PinEntry.empty(4).fillWith("1788"), confirmPinEntry = PinEntry.createEmpty(4).fillWith("1788"),
isConfirmationStep = true, isConfirmationStep = true,
creationFailure = SetupPinFailure.PinsDontMatch creationFailure = SetupPinFailure.PinsDontMatch
), ),
aSetupPinState( aSetupPinState(
choosePinEntry = PinEntry.empty(4).fillWith("1111"), choosePinEntry = PinEntry.createEmpty(4).fillWith("1111"),
creationFailure = SetupPinFailure.PinBlacklisted creationFailure = SetupPinFailure.PinBlacklisted
), ),
@ -46,8 +46,8 @@ open class SetupPinStateProvider : PreviewParameterProvider<SetupPinState> {
} }
fun aSetupPinState( fun aSetupPinState(
choosePinEntry: PinEntry = PinEntry.empty(4), choosePinEntry: PinEntry = PinEntry.createEmpty(4),
confirmPinEntry: PinEntry = PinEntry.empty(4), confirmPinEntry: PinEntry = PinEntry.createEmpty(4),
isConfirmationStep: Boolean = false, isConfirmationStep: Boolean = false,
creationFailure: SetupPinFailure? = null, creationFailure: SetupPinFailure? = null,
) = SetupPinState( ) = SetupPinState(

View file

@ -16,17 +16,12 @@
package io.element.android.features.lockscreen.impl.setup.validation package io.element.android.features.lockscreen.impl.setup.validation
import androidx.annotation.VisibleForTesting import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import javax.inject.Inject import javax.inject.Inject
class PinValidator @Inject constructor() { class PinValidator @Inject constructor() {
companion object {
@VisibleForTesting
val BLACKLIST = listOf("0000", "1234")
}
sealed interface Result { sealed interface Result {
data object Valid : Result data object Valid : Result
data class Invalid(val failure: SetupPinFailure) : Result data class Invalid(val failure: SetupPinFailure) : Result
@ -34,7 +29,7 @@ class PinValidator @Inject constructor() {
fun isPinValid(pinEntry: PinEntry): Result { fun isPinValid(pinEntry: PinEntry): Result {
val pinAsText = pinEntry.toText() val pinAsText = pinEntry.toText()
val isBlacklisted = BLACKLIST.any { it == pinAsText } val isBlacklisted = LockScreenConfig.PIN_BLACKLIST.any { it == pinAsText }
return if (isBlacklisted) { return if (isBlacklisted) {
Result.Invalid(SetupPinFailure.PinBlacklisted) Result.Invalid(SetupPinFailure.PinBlacklisted)
} else { } else {

View file

@ -16,7 +16,7 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
sealed interface PinUnlockEvents { sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents

View file

@ -23,9 +23,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.lockscreen.api.LockScreenStateService
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,9 +40,11 @@ class PinUnlockPresenter @Inject constructor(
@Composable @Composable
override fun present(): PinUnlockState { override fun present(): PinUnlockState {
var pinEntry by remember { var pinEntry by remember {
mutableStateOf(PinEntry.empty(4)) //TODO fetch size from db
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
} }
var remainingAttempts by rememberSaveable { var remainingAttempts by rememberSaveable {
//TODO fetch from db
mutableIntStateOf(3) mutableIntStateOf(3)
} }
var showWrongPinTitle by rememberSaveable { var showWrongPinTitle by rememberSaveable {
@ -56,6 +59,7 @@ class PinUnlockPresenter @Inject constructor(
is PinUnlockEvents.OnPinKeypadPressed -> { is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntry = pinEntry.process(event.pinKeypadModel) pinEntry = pinEntry.process(event.pinKeypadModel)
if (pinEntry.isComplete()) { if (pinEntry.isComplete()) {
//TODO check pin with PinCodeManager
coroutineScope.launch { pinStateService.unlock() } coroutineScope.launch { pinStateService.unlock() }
} }
} }

View file

@ -23,7 +23,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState> override val values: Sequence<PinUnlockState>
get() = sequenceOf( get() = sequenceOf(
aPinUnlockState(), aPinUnlockState(),
aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")), aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")),
aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showWrongPinTitle = true),
aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
@ -31,7 +31,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
} }
fun aPinUnlockState( fun aPinUnlockState(
pinEntry: PinEntry = PinEntry.empty(4), pinEntry: PinEntry = PinEntry.createEmpty(4),
remainingAttempts: Int = 3, remainingAttempts: Int = 3,
showWrongPinTitle: Boolean = false, showWrongPinTitle: Boolean = false,
showSignOutPrompt: Boolean = false, showSignOutPrompt: Boolean = false,

View file

@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinDigit
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.lockscreen.impl.unlock.numpad package io.element.android.features.lockscreen.impl.unlock.keypad
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.lockscreen.impl.unlock.numpad package io.element.android.features.lockscreen.impl.unlock.keypad
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable

View file

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertEmpty
import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
@ -31,7 +32,7 @@ import org.junit.Test
class SetupPinPresenterTest { class SetupPinPresenterTest {
private val blacklistedPin = PinValidator.BLACKLIST.first() private val blacklistedPin = LockScreenConfig.PIN_BLACKLIST
private val halfCompletePin = "12" private val halfCompletePin = "12"
private val completePin = "1235" private val completePin = "1235"
private val mismatchedPin = "1236" private val mismatchedPin = "1236"

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertEmpty
import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem