Pin unlock : start branching logic
This commit is contained in:
parent
56a612e1c5
commit
9822d8825e
10 changed files with 85 additions and 31 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue