Pin unlock : add signout prompt

This commit is contained in:
ganfra
2023-10-20 18:52:56 +02:00
parent 02c5873fc9
commit d12fa5c8fa
10 changed files with 74 additions and 12 deletions

View File

@@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.cryptography.api)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View File

@@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor(
return pinCodeStore.hasPinCode()
}
override suspend fun SetupPinCode(pinCode: String) {
override suspend fun setupPinCode(pinCode: String) {
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)

View File

@@ -30,7 +30,7 @@ interface PinCodeManager {
* Creates a new encrypted pin code.
* @param pinCode the clear pin code to create
*/
suspend fun SetupPinCode(pinCode: String)
suspend fun setupPinCode(pinCode: String)
/**
* @return true if the pin code is correct.

View File

@@ -18,10 +18,11 @@ package io.element.android.features.lockscreen.impl.pin.model
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.io.Serializable
data class PinEntry(
val digits: ImmutableList<PinDigit>,
) {
): Serializable {
companion object {
fun empty(size: Int): PinEntry {

View File

@@ -21,4 +21,5 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel
sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
data object Unlock : PinUnlockEvents
data object OnForgetPin : PinUnlockEvents
}

View File

@@ -18,8 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.lockscreen.api.LockScreenStateService
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
@@ -36,10 +37,18 @@ class PinUnlockPresenter @Inject constructor(
@Composable
override fun present(): PinUnlockState {
var pinEntry by remember {
var pinEntry by rememberSaveable {
mutableStateOf(PinEntry.empty(4))
}
var remainingAttempts by rememberSaveable {
mutableIntStateOf(3)
}
var showWrongPinTitle by rememberSaveable {
mutableStateOf(false)
}
var showSignOutPrompt by rememberSaveable {
mutableStateOf(false)
}
fun handleEvents(event: PinUnlockEvents) {
when (event) {
@@ -50,10 +59,14 @@ class PinUnlockPresenter @Inject constructor(
coroutineScope.launch { pinStateService.unlock() }
}
}
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
}
}
return PinUnlockState(
pinEntry = pinEntry,
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = remainingAttempts,
showSignOutPrompt = showSignOutPrompt,
eventSink = ::handleEvents
)
}

View File

@@ -20,5 +20,10 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
data class PinUnlockState(
val pinEntry: PinEntry,
val showWrongPinTitle: Boolean,
val remainingAttempts: Int,
val showSignOutPrompt: Boolean,
val eventSink: (PinUnlockEvents) -> Unit
)
) {
val isSignOutPromptCancellable = remainingAttempts > 0
}

View File

@@ -23,12 +23,22 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState>
get() = sequenceOf(
aPinUnlockState(),
aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")),
aPinUnlockState(showWrongPinTitle = true),
aPinUnlockState(showSignOutPrompt = true),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
)
}
fun aPinUnlockState(
pinEntry: PinEntry = PinEntry.empty(4),
remainingAttempts: Int = 3,
showWrongPinTitle: Boolean = false,
showSignOutPrompt: Boolean = false,
) = PinUnlockState(
pinEntry = pinEntry,
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = remainingAttempts,
showSignOutPrompt = showSignOutPrompt,
eventSink = {}
)

View File

@@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -46,6 +47,8 @@ import io.element.android.features.lockscreen.impl.R
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.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -53,6 +56,7 @@ 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.TextButton
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PinUnlockView(
@@ -101,6 +105,22 @@ fun PinUnlockView(
modifier = commonModifier,
)
}
if (state.showSignOutPrompt) {
if (state.isSignOutPromptCancellable) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onSubmitClicked = {},
onDismiss = {},
)
} else {
ErrorDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onDismiss = {},
)
}
}
}
}
}
@@ -196,7 +216,7 @@ private fun PinUnlockHeader(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Enter your PIN",
text = stringResource(id = CommonStrings.common_enter_your_pin),
modifier = Modifier
.fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -204,12 +224,22 @@ private fun PinUnlockHeader(
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.height(8.dp))
val subtitle = if (state.showWrongPinTitle) {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts)
} else {
stringResource(id = R.string.screen_app_lock_subtitle)
}
val subtitleColor = if (state.showWrongPinTitle) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.secondary
}
Text(
text = "You have 3 attempts to unlock",
text = subtitle,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
color = subtitleColor,
)
Spacer(Modifier.height(24.dp))
PinDotsRow(state.pinEntry)

View File

@@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.Feature
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@@ -44,10 +45,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con
}
}
override suspend fun isFeatureEnabled(feature: Feature): Boolean {
override fun isFeatureEnabled(feature: Feature): Flow<Boolean> {
return store.data.map { prefs ->
prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue
}.first()
}
}
override fun hasFeature(feature: Feature): Boolean {