From 67d4271f4d81e701517de1b61ac4399d88c571de Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 18:52:56 +0200 Subject: [PATCH] Pin unlock : add signout prompt --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- .../lockscreen/impl/pin/model/PinEntry.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 1 + .../impl/unlock/PinUnlockPresenter.kt | 19 ++++++++-- .../lockscreen/impl/unlock/PinUnlockState.kt | 7 +++- .../impl/unlock/PinUnlockStateProvider.kt | 10 ++++++ .../lockscreen/impl/unlock/PinUnlockView.kt | 36 +++++++++++++++++-- .../impl/PreferencesFeatureFlagProvider.kt | 5 +-- 10 files changed, 74 insertions(+), 12 deletions(-) diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 028d8bee3c..8daeb2178c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -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) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 1f7439301c..f5848a9d40 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -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) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 49b6141665..09197c3eb1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -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. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 92dda869a6..76331bd38f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -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, -) { +): Serializable { companion object { fun empty(size: Int): PinEntry { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index 8dddd40e8a..a90b6cb702 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -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 } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index d673ad7966..d6b77f799c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -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 ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 69d3213c7e..1787fb8e8b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -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 +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 5120818316..4f269d2f5a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,12 +23,22 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence 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 = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 4f4f9be2df..19cb866a5c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -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) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index ddffdebd34..23ff977da2 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -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 { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - }.first() + } } override fun hasFeature(feature: Feature): Boolean {