Pin unlock : add signout prompt
This commit is contained in:
parent
02c5873fc9
commit
d12fa5c8fa
10 changed files with 74 additions and 12 deletions
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.cryptography.api)
|
implementation(projects.libraries.cryptography.api)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
return pinCodeStore.hasPinCode()
|
return pinCodeStore.hasPinCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun SetupPinCode(pinCode: String) {
|
override suspend fun setupPinCode(pinCode: String) {
|
||||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||||
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
||||||
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
|
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ interface PinCodeManager {
|
||||||
* Creates a new encrypted pin code.
|
* Creates a new encrypted pin code.
|
||||||
* @param pinCode the clear pin code to create
|
* @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.
|
* @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.ImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
data class PinEntry(
|
data class PinEntry(
|
||||||
val digits: ImmutableList<PinDigit>,
|
val digits: ImmutableList<PinDigit>,
|
||||||
) {
|
): Serializable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun empty(size: Int): PinEntry {
|
fun empty(size: Int): PinEntry {
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,5 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel
|
||||||
sealed interface PinUnlockEvents {
|
sealed interface PinUnlockEvents {
|
||||||
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
|
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
|
||||||
data object Unlock : 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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.pin.model.PinEntry
|
||||||
|
|
@ -36,10 +37,18 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinUnlockState {
|
override fun present(): PinUnlockState {
|
||||||
|
var pinEntry by rememberSaveable {
|
||||||
var pinEntry by remember {
|
|
||||||
mutableStateOf(PinEntry.empty(4))
|
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) {
|
fun handleEvents(event: PinUnlockEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -50,10 +59,14 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
coroutineScope.launch { pinStateService.unlock() }
|
coroutineScope.launch { pinStateService.unlock() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PinUnlockState(
|
return PinUnlockState(
|
||||||
pinEntry = pinEntry,
|
pinEntry = pinEntry,
|
||||||
|
showWrongPinTitle = showWrongPinTitle,
|
||||||
|
remainingAttempts = remainingAttempts,
|
||||||
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,10 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
|
|
||||||
data class PinUnlockState(
|
data class PinUnlockState(
|
||||||
val pinEntry: PinEntry,
|
val pinEntry: PinEntry,
|
||||||
|
val showWrongPinTitle: Boolean,
|
||||||
|
val remainingAttempts: Int,
|
||||||
|
val showSignOutPrompt: Boolean,
|
||||||
val eventSink: (PinUnlockEvents) -> Unit
|
val eventSink: (PinUnlockEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
val isSignOutPromptCancellable = remainingAttempts > 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,22 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||||
override val values: Sequence<PinUnlockState>
|
override val values: Sequence<PinUnlockState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aPinUnlockState(),
|
aPinUnlockState(),
|
||||||
|
aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")),
|
||||||
|
aPinUnlockState(showWrongPinTitle = true),
|
||||||
|
aPinUnlockState(showSignOutPrompt = true),
|
||||||
|
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aPinUnlockState(
|
fun aPinUnlockState(
|
||||||
pinEntry: PinEntry = PinEntry.empty(4),
|
pinEntry: PinEntry = PinEntry.empty(4),
|
||||||
|
remainingAttempts: Int = 3,
|
||||||
|
showWrongPinTitle: Boolean = false,
|
||||||
|
showSignOutPrompt: Boolean = false,
|
||||||
) = PinUnlockState(
|
) = PinUnlockState(
|
||||||
pinEntry = pinEntry,
|
pinEntry = pinEntry,
|
||||||
|
showWrongPinTitle = showWrongPinTitle,
|
||||||
|
remainingAttempts = remainingAttempts,
|
||||||
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
|
|
@ -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.PinDigit
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
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.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.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.Icon
|
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.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PinUnlockView(
|
fun PinUnlockView(
|
||||||
|
|
@ -101,6 +105,22 @@ fun PinUnlockView(
|
||||||
modifier = commonModifier,
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Enter your PIN",
|
text = stringResource(id = CommonStrings.common_enter_your_pin),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
@ -204,12 +224,22 @@ private fun PinUnlockHeader(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
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(
|
||||||
text = "You have 3 attempts to unlock",
|
text = subtitle,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = subtitleColor,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
PinDotsRow(state.pinEntry)
|
PinDotsRow(state.pinEntry)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.featureflag.api.Feature
|
import io.element.android.libraries.featureflag.api.Feature
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
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 ->
|
return store.data.map { prefs ->
|
||||||
prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue
|
prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue
|
||||||
}.first()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasFeature(feature: Feature): Boolean {
|
override fun hasFeature(feature: Feature): Boolean {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue