diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index f492f5ab05..9e53762c07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -18,4 +18,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents + data object OnClearValidationFailure : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 4599a06fc9..18a17acb62 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -20,28 +20,73 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.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 javax.inject.Inject -class CreatePinPresenter @Inject constructor() : Presenter { +private const val PIN_SIZE = 4 + +class CreatePinPresenter @Inject constructor( + private val pinValidator: PinValidator, + private val pinCodeManager: PinCodeManager, +) : Presenter { @Composable override fun present(): CreatePinState { - val pinEntry by remember { - mutableStateOf(PinEntry.empty(4)) + var choosePinEntry by remember { + 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(null) } fun handleEvents(event: CreatePinEvents) { when (event) { 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( - pinEntry = pinEntry, + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 9b3835193f..799d4b20a8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -17,8 +17,18 @@ 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.validation.PinCreationFailure data class CreatePinState( - val pinEntry: PinEntry, + val choosePinEntry: PinEntry, + val confirmPinEntry: PinEntry, + val isConfirmationStep: Boolean, + val creationFailure: PinCreationFailure?, val eventSink: (CreatePinEvents) -> Unit -) +) { + val activePinEntry = if (isConfirmationStep) { + confirmPinEntry + } else { + choosePinEntry + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index dbce5dddc5..f4d778a296 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -17,26 +17,33 @@ package io.element.android.features.lockscreen.impl.create 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 kotlinx.collections.immutable.persistentListOf +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreatePinState(), - // Add other states here + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("12") + ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1789"), + isConfirmationStep = true, + ), ) } -fun aCreatePinState() = CreatePinState( - pinEntry = PinEntry( - digits = persistentListOf( - PinDigit.Filled('1'), - PinDigit.Filled('2'), - PinDigit.Empty, - PinDigit.Empty, - ) - ), +fun aCreatePinState( + choosePinEntry: PinEntry = PinEntry.empty(4), + confirmPinEntry: PinEntry = PinEntry.empty(4), + isConfirmationStep: Boolean = false, + creationFailure: PinCreationFailure? = null, +) = CreatePinState( + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = {} ) + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 6035f9d5d6..f5b2e49df8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -75,7 +75,7 @@ fun CreatePinView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - header = { CreatePinHeader() }, + header = { CreatePinHeader(state.isConfirmationStep) }, footer = { CreatePinFooter() }, content = { CreatePinContent(state) } ) @@ -85,11 +85,12 @@ fun CreatePinView( @Composable private fun CreatePinHeader( + isValidationStep: Boolean, modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( 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", iconImageVector = Icons.Default.Lock, ) @@ -111,9 +112,8 @@ private fun CreatePinContent( state: CreatePinState, modifier: Modifier = Modifier, ) { - PinEntryTextField( - state.pinEntry, + state.activePinEntry, onValueChange = { state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) }, @@ -135,7 +135,7 @@ fun PinEntryTextField( onValueChange = { onValueChange(it.text) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), decorationBox = { PinEntryRow(pinEntry = pinEntry) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt index 587fde955d..2228110156 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -50,6 +50,10 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } + fun clear(): PinEntry { + return fillWith("") + } + fun isPinComplete(): Boolean { return digits.all { it is PinDigit.Filled } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt new file mode 100644 index 0000000000..26b1eb5fd8 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt @@ -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 +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt new file mode 100644 index 0000000000..1d97cda60d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -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 + } + } +}