Pin: add LockScreenConfig and address PR reviews
This commit is contained in:
parent
bf8cf00c51
commit
d21623e523
15 changed files with 71 additions and 38 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue