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())
}
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 {
return fillWith("")
}
fun isPinComplete(): Boolean {
fun isComplete(): Boolean {
return digits.all { it is PinDigit.Filled }
}
fun isEmpty(): Boolean {
return digits.all { it is PinDigit.Empty }
}
fun toText(): String {
return digits.joinToString("") {
it.toText()

View File

@@ -55,7 +55,7 @@ class SetupPinPresenter @Inject constructor(
is SetupPinEvents.OnPinEntryChanged -> {
if (isConfirmationStep) {
confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText)
if (confirmPinEntry.isPinComplete()) {
if (confirmPinEntry.isComplete()) {
if (confirmPinEntry == choosePinEntry) {
//TODO save in db and navigate to next screen
} else {
@@ -64,7 +64,7 @@ class SetupPinPresenter @Inject constructor(
}
} else {
choosePinEntry = choosePinEntry.fillWith(event.entryAsText)
if (choosePinEntry.isPinComplete()) {
if (choosePinEntry.isComplete()) {
when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) {
is PinValidator.Result.Invalid -> {
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 kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -57,7 +56,7 @@ class DefaultLockScreenStateService @Inject constructor(
override suspend fun entersBackground() = coroutineScope {
lockJob = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
delay(GRACE_PERIOD_IN_MILLIS)
//delay(GRACE_PERIOD_IN_MILLIS)
_lockScreenState.value = LockScreenState.Locked
}
}

View File

@@ -16,6 +16,9 @@
package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel
sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
data object Unlock : PinUnlockEvents
}

View File

@@ -17,7 +17,13 @@
package io.element.android.features.lockscreen.impl.unlock
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.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -31,13 +37,32 @@ class PinUnlockPresenter @Inject constructor(
@Composable
override fun present(): PinUnlockState {
var pinEntry by remember {
mutableStateOf(PinEntry.empty(4))
}
fun handleEvents(event: PinUnlockEvents) {
when (event) {
PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() }
is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntry = pinEntry.process(event.pinKeypadModel)
if (pinEntry.isComplete()) {
coroutineScope.launch { pinStateService.unlock() }
}
}
}
}
return PinUnlockState(
pinEntry = pinEntry,
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
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
data class PinUnlockState(
val pinEntry: PinEntry,
val eventSink: (PinUnlockEvents) -> Unit
)

View File

@@ -17,6 +17,7 @@
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
open class PinUnlockStateProvider : PreviewParameterProvider<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 = {}
)

View File

@@ -36,11 +36,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
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.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview
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.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@@ -53,7 +54,12 @@ fun PinUnlockView(
) {
Surface(modifier) {
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 = {
Box(
modifier = Modifier
@@ -62,7 +68,9 @@ fun PinUnlockView(
contentAlignment = Alignment.Center,
) {
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
private fun PinDotsRow(
pinEntry: PinEntry,
modifier: Modifier = Modifier,
) {
Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
PinDot(isFilled = true)
PinDot(isFilled = true)
PinDot(isFilled = false)
PinDot(isFilled = false)
for (digit in pinEntry.digits) {
PinDot(isFilled = digit is PinDigit.Filled)
}
}
}
@@ -112,6 +109,7 @@ private fun PinDot(
@Composable
private fun PinUnlockHeader(
state: PinUnlockState,
modifier: Modifier = Modifier,
) {
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
@@ -140,7 +138,7 @@ private fun PinUnlockHeader(
color = MaterialTheme.colorScheme.secondary,
)
Spacer(Modifier.height(24.dp))
PinDotsRow()
PinDotsRow(state.pinEntry)
}
}

View File

@@ -61,25 +61,25 @@ fun PinKeypad(
PinKeypadRow(
verticalAlignment = verticalAlignment,
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,
)
PinKeypadRow(
verticalAlignment = verticalAlignment,
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,
)
PinKeypadRow(
verticalAlignment = verticalAlignment,
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,
)
PinKeypadRow(
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back),
models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back),
onClick = onClick,
)
}
@@ -114,7 +114,7 @@ private fun PinKeypadRow(
PinKeyBadDigitButton(
size = 80.dp,
modifier = commonModifier,
digit = model.number,
digit = model.number.toString(),
onClick = { onClick(model) },
)
}

View File

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