Pin create : add some more states to manage validation and confirmation

This commit is contained in:
ganfra 2023-10-19 12:11:14 +02:00
parent f07a687630
commit 63fbb4412b
8 changed files with 153 additions and 24 deletions

View file

@ -18,4 +18,5 @@ package io.element.android.features.lockscreen.impl.create
sealed interface CreatePinEvents { sealed interface CreatePinEvents {
data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents
data object OnClearValidationFailure : CreatePinEvents
} }

View file

@ -20,28 +20,73 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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 io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.model.PinEntry
import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure
import io.element.android.features.lockscreen.impl.create.validation.PinValidator
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class CreatePinPresenter @Inject constructor() : Presenter<CreatePinState> { private const val PIN_SIZE = 4
class CreatePinPresenter @Inject constructor(
private val pinValidator: PinValidator,
private val pinCodeManager: PinCodeManager,
) : Presenter<CreatePinState> {
@Composable @Composable
override fun present(): CreatePinState { override fun present(): CreatePinState {
val pinEntry by remember { var choosePinEntry by remember {
mutableStateOf(PinEntry.empty(4)) mutableStateOf(PinEntry.empty(PIN_SIZE))
}
var confirmPinEntry by remember {
mutableStateOf(PinEntry.empty(PIN_SIZE))
}
var isConfirmationStep by remember {
mutableStateOf(false)
}
var creationFailure by remember {
mutableStateOf<PinCreationFailure?>(null)
} }
fun handleEvents(event: CreatePinEvents) { fun handleEvents(event: CreatePinEvents) {
when (event) { when (event) {
is CreatePinEvents.OnPinEntryChanged -> { is CreatePinEvents.OnPinEntryChanged -> {
pinEntry.fillWith(event.entryAsText) if (isConfirmationStep) {
confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText)
if (confirmPinEntry.isPinComplete()) {
if (confirmPinEntry == choosePinEntry) {
//pinCodeManager.savePin(confirmPinEntry.toText())
} else {
confirmPinEntry = PinEntry.empty(PIN_SIZE)
creationFailure = PinCreationFailure.ConfirmationPinNotMatching
}
}
} else {
choosePinEntry = choosePinEntry.fillWith(event.entryAsText)
if (choosePinEntry.isPinComplete()) {
when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) {
is PinValidator.Result.Invalid -> {
choosePinEntry = PinEntry.empty(PIN_SIZE)
creationFailure = pinValidationResult.failure
}
PinValidator.Result.Valid -> isConfirmationStep = true
}
}
}
}
CreatePinEvents.OnClearValidationFailure -> {
creationFailure = null
} }
} }
} }
return CreatePinState( return CreatePinState(
pinEntry = pinEntry, choosePinEntry = choosePinEntry,
confirmPinEntry = confirmPinEntry,
isConfirmationStep = isConfirmationStep,
creationFailure = creationFailure,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -17,8 +17,18 @@
package io.element.android.features.lockscreen.impl.create package io.element.android.features.lockscreen.impl.create
import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.model.PinEntry
import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure
data class CreatePinState( data class CreatePinState(
val pinEntry: PinEntry, val choosePinEntry: PinEntry,
val confirmPinEntry: PinEntry,
val isConfirmationStep: Boolean,
val creationFailure: PinCreationFailure?,
val eventSink: (CreatePinEvents) -> Unit val eventSink: (CreatePinEvents) -> Unit
) ) {
val activePinEntry = if (isConfirmationStep) {
confirmPinEntry
} else {
choosePinEntry
}
}

View file

@ -17,26 +17,33 @@
package io.element.android.features.lockscreen.impl.create package io.element.android.features.lockscreen.impl.create
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.create.model.PinDigit
import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.model.PinEntry
import kotlinx.collections.immutable.persistentListOf import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure
open class CreatePinStateProvider : PreviewParameterProvider<CreatePinState> { open class CreatePinStateProvider : PreviewParameterProvider<CreatePinState> {
override val values: Sequence<CreatePinState> override val values: Sequence<CreatePinState>
get() = sequenceOf( get() = sequenceOf(
aCreatePinState(), aCreatePinState(),
// Add other states here aCreatePinState(
choosePinEntry = PinEntry.empty(4).fillWith("12")
),
aCreatePinState(
choosePinEntry = PinEntry.empty(4).fillWith("1789"),
isConfirmationStep = true,
),
) )
} }
fun aCreatePinState() = CreatePinState( fun aCreatePinState(
pinEntry = PinEntry( choosePinEntry: PinEntry = PinEntry.empty(4),
digits = persistentListOf( confirmPinEntry: PinEntry = PinEntry.empty(4),
PinDigit.Filled('1'), isConfirmationStep: Boolean = false,
PinDigit.Filled('2'), creationFailure: PinCreationFailure? = null,
PinDigit.Empty, ) = CreatePinState(
PinDigit.Empty, choosePinEntry = choosePinEntry,
) confirmPinEntry = confirmPinEntry,
), isConfirmationStep = isConfirmationStep,
creationFailure = creationFailure,
eventSink = {} eventSink = {}
) )

View file

@ -75,7 +75,7 @@ fun CreatePinView(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
header = { CreatePinHeader() }, header = { CreatePinHeader(state.isConfirmationStep) },
footer = { CreatePinFooter() }, footer = { CreatePinFooter() },
content = { CreatePinContent(state) } content = { CreatePinContent(state) }
) )
@ -85,11 +85,12 @@ fun CreatePinView(
@Composable @Composable
private fun CreatePinHeader( private fun CreatePinHeader(
isValidationStep: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier, modifier = modifier,
title = "Choose 4 digit PIN", title = if (isValidationStep) "Confirm PIN" else "Choose 4 digit PIN",
subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app",
iconImageVector = Icons.Default.Lock, iconImageVector = Icons.Default.Lock,
) )
@ -111,9 +112,8 @@ private fun CreatePinContent(
state: CreatePinState, state: CreatePinState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
PinEntryTextField( PinEntryTextField(
state.pinEntry, state.activePinEntry,
onValueChange = { onValueChange = {
state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) state.eventSink(CreatePinEvents.OnPinEntryChanged(it))
}, },
@ -135,7 +135,7 @@ fun PinEntryTextField(
onValueChange = { onValueChange = {
onValueChange(it.text) onValueChange(it.text)
}, },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
decorationBox = { decorationBox = {
PinEntryRow(pinEntry = pinEntry) PinEntryRow(pinEntry = pinEntry)
} }

View file

@ -50,6 +50,10 @@ data class PinEntry(
return copy(digits = newDigits.toPersistentList()) return copy(digits = newDigits.toPersistentList())
} }
fun clear(): PinEntry {
return fillWith("")
}
fun isPinComplete(): Boolean { fun isPinComplete(): Boolean {
return digits.all { it is PinDigit.Filled } return digits.all { it is PinDigit.Filled }
} }

View file

@ -0,0 +1,22 @@
/*
* 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.features.lockscreen.impl.create.validation
sealed interface PinCreationFailure {
data object ChosenPinBlacklisted : PinCreationFailure
data object ConfirmationPinNotMatching : PinCreationFailure
}

View file

@ -0,0 +1,40 @@
/*
* 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.features.lockscreen.impl.create.validation
import io.element.android.features.lockscreen.impl.create.model.PinEntry
import javax.inject.Inject
private val BLACKLIST = listOf("0000", "1234")
class PinValidator @Inject constructor() {
sealed interface Result {
data object Valid : Result
data class Invalid(val failure: PinCreationFailure) : Result
}
fun isPinValid(pinEntry: PinEntry): Result {
val pinAsText = pinEntry.toText()
val isBlacklisted = BLACKLIST.any { it == pinAsText }
return if (isBlacklisted) {
Result.Invalid(PinCreationFailure.ChosenPinBlacklisted)
} else {
Result.Valid
}
}
}