Browse Source

Pin unlock : add signout prompt

pull/1624/head
ganfra 11 months ago
parent
commit
67d4271f4d
  1. 1
      features/lockscreen/impl/build.gradle.kts
  2. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
  3. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
  4. 3
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
  5. 1
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
  6. 19
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
  7. 7
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
  8. 10
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
  9. 36
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
  10. 5
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt

1
features/lockscreen/impl/build.gradle.kts

@ -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)

2
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() 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)

2
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. * 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.

3
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.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 {

1
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 { 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
} }

19
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.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
) )
} }

7
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( 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
}

10
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt

@ -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 = {}
) )

36
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.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)

5
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 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…
Cancel
Save