Pin unlock : start branching logic

This commit is contained in:
ganfra 2023-10-20 15:43:55 +02:00
parent 56a612e1c5
commit 9822d8825e
10 changed files with 85 additions and 31 deletions

View file

@ -50,14 +50,36 @@ data class PinEntry(
return copy(digits = newDigits.toPersistentList()) return copy(digits = newDigits.toPersistentList())
} }
fun deleteLast(): PinEntry {
if (isEmpty()) return this
val newDigits = digits.toMutableList()
newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled ->
newDigits[lastFilled] = PinDigit.Empty
}
return copy(digits = newDigits.toPersistentList())
}
fun addDigit(digit: Char): PinEntry {
if (isComplete()) return this
val newDigits = digits.toMutableList()
newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty ->
newDigits[firstEmpty] = PinDigit.Filled(digit)
}
return copy(digits = newDigits.toPersistentList())
}
fun clear(): PinEntry { fun clear(): PinEntry {
return fillWith("") return fillWith("")
} }
fun isPinComplete(): Boolean { fun isComplete(): Boolean {
return digits.all { it is PinDigit.Filled } return digits.all { it is PinDigit.Filled }
} }
fun isEmpty(): Boolean {
return digits.all { it is PinDigit.Empty }
}
fun toText(): String { fun toText(): String {
return digits.joinToString("") { return digits.joinToString("") {
it.toText() it.toText()

View file

@ -55,7 +55,7 @@ class SetupPinPresenter @Inject constructor(
is SetupPinEvents.OnPinEntryChanged -> { is SetupPinEvents.OnPinEntryChanged -> {
if (isConfirmationStep) { if (isConfirmationStep) {
confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText)
if (confirmPinEntry.isPinComplete()) { if (confirmPinEntry.isComplete()) {
if (confirmPinEntry == choosePinEntry) { if (confirmPinEntry == choosePinEntry) {
//TODO save in db and navigate to next screen //TODO save in db and navigate to next screen
} else { } else {
@ -64,7 +64,7 @@ class SetupPinPresenter @Inject constructor(
} }
} else { } else {
choosePinEntry = choosePinEntry.fillWith(event.entryAsText) choosePinEntry = choosePinEntry.fillWith(event.entryAsText)
if (choosePinEntry.isPinComplete()) { if (choosePinEntry.isComplete()) {
when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) {
is PinValidator.Result.Invalid -> { is PinValidator.Result.Invalid -> {
setupPinFailure = pinValidationResult.failure setupPinFailure = pinValidationResult.failure

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -57,7 +56,7 @@ class DefaultLockScreenStateService @Inject constructor(
override suspend fun entersBackground() = coroutineScope { override suspend fun entersBackground() = coroutineScope {
lockJob = launch { lockJob = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
delay(GRACE_PERIOD_IN_MILLIS) //delay(GRACE_PERIOD_IN_MILLIS)
_lockScreenState.value = LockScreenState.Locked _lockScreenState.value = LockScreenState.Locked
} }
} }

View file

@ -16,6 +16,9 @@
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
sealed interface PinUnlockEvents { sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
data object Unlock : PinUnlockEvents data object Unlock : PinUnlockEvents
} }

View file

@ -17,7 +17,13 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable 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.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.unlock.numpad.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
@ -31,13 +37,32 @@ class PinUnlockPresenter @Inject constructor(
@Composable @Composable
override fun present(): PinUnlockState { override fun present(): PinUnlockState {
var pinEntry by remember {
mutableStateOf(PinEntry.empty(4))
}
fun handleEvents(event: PinUnlockEvents) { fun handleEvents(event: PinUnlockEvents) {
when (event) { when (event) {
PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() }
is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntry = pinEntry.process(event.pinKeypadModel)
if (pinEntry.isComplete()) {
coroutineScope.launch { pinStateService.unlock() }
}
}
} }
} }
return PinUnlockState( return PinUnlockState(
pinEntry = pinEntry,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry {
return when (pinKeypadModel) {
PinKeypadModel.Back -> deleteLast()
is PinKeypadModel.Number -> addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> this
}
}
} }

View file

@ -16,6 +16,9 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
data class PinUnlockState( data class PinUnlockState(
val pinEntry: PinEntry,
val eventSink: (PinUnlockEvents) -> Unit val eventSink: (PinUnlockEvents) -> Unit
) )

View file

@ -17,6 +17,7 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> { open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState> override val values: Sequence<PinUnlockState>
@ -25,6 +26,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
) )
} }
fun aPinUnlockState() = PinUnlockState( fun aPinUnlockState(
pinEntry: PinEntry = PinEntry.empty(4),
) = PinUnlockState(
pinEntry = pinEntry,
eventSink = {} eventSink = {}
) )

View file

@ -36,11 +36,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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.unlock.numpad.PinKeypad import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
@ -53,7 +54,12 @@ fun PinUnlockView(
) { ) {
Surface(modifier) { Surface(modifier) {
HeaderFooterPage( HeaderFooterPage(
header = { PinUnlockHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, header = {
PinUnlockHeader(
state = state,
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)
)
},
content = { content = {
Box( Box(
modifier = Modifier modifier = Modifier
@ -62,7 +68,9 @@ fun PinUnlockView(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
PinKeypad( PinKeypad(
onClick = {} onClick = {
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
}
) )
} }
} }
@ -70,26 +78,15 @@ fun PinUnlockView(
} }
} }
@Composable
private fun PinUnlockFooter(state: PinUnlockState) {
Button(
modifier = Modifier.fillMaxWidth(),
text = "Unlock",
onClick = {
state.eventSink(PinUnlockEvents.Unlock)
}
)
}
@Composable @Composable
private fun PinDotsRow( private fun PinDotsRow(
pinEntry: PinEntry,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
PinDot(isFilled = true) for (digit in pinEntry.digits) {
PinDot(isFilled = true) PinDot(isFilled = digit is PinDigit.Filled)
PinDot(isFilled = false) }
PinDot(isFilled = false)
} }
} }
@ -112,6 +109,7 @@ private fun PinDot(
@Composable @Composable
private fun PinUnlockHeader( private fun PinUnlockHeader(
state: PinUnlockState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
@ -140,7 +138,7 @@ private fun PinUnlockHeader(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
PinDotsRow() PinDotsRow(state.pinEntry)
} }
} }

View file

@ -61,25 +61,25 @@ fun PinKeypad(
PinKeypadRow( PinKeypadRow(
verticalAlignment = verticalAlignment, verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement, horizontalArrangement = horizontalArrangement,
models = listOf(PinKeypadModel.Number("1"), PinKeypadModel.Number("2"), PinKeypadModel.Number("3")), models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')),
onClick = onClick, onClick = onClick,
) )
PinKeypadRow( PinKeypadRow(
verticalAlignment = verticalAlignment, verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement, horizontalArrangement = horizontalArrangement,
models = listOf(PinKeypadModel.Number("4"), PinKeypadModel.Number("5"), PinKeypadModel.Number("6")), models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')),
onClick = onClick, onClick = onClick,
) )
PinKeypadRow( PinKeypadRow(
verticalAlignment = verticalAlignment, verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement, horizontalArrangement = horizontalArrangement,
models = listOf(PinKeypadModel.Number("7"), PinKeypadModel.Number("8"), PinKeypadModel.Number("9")), models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')),
onClick = onClick, onClick = onClick,
) )
PinKeypadRow( PinKeypadRow(
verticalAlignment = verticalAlignment, verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement, horizontalArrangement = horizontalArrangement,
models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back), models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back),
onClick = onClick, onClick = onClick,
) )
} }
@ -114,7 +114,7 @@ private fun PinKeypadRow(
PinKeyBadDigitButton( PinKeyBadDigitButton(
size = 80.dp, size = 80.dp,
modifier = commonModifier, modifier = commonModifier,
digit = model.number, digit = model.number.toString(),
onClick = { onClick(model) }, onClick = { onClick(model) },
) )
} }

View file

@ -22,5 +22,5 @@ import androidx.compose.runtime.Immutable
sealed interface PinKeypadModel { sealed interface PinKeypadModel {
data object Empty : PinKeypadModel data object Empty : PinKeypadModel
data object Back : PinKeypadModel data object Back : PinKeypadModel
data class Number(val number: String) : PinKeypadModel data class Number(val number: Char) : PinKeypadModel
} }