Pin create : add some more states to manage validation and confirmation
This commit is contained in:
parent
f07a687630
commit
63fbb4412b
8 changed files with 153 additions and 24 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue