Pin unlock : add signout prompt
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user