Merge pull request #1714 from vector-im/feature/fga/unlock_settings_2
Pin unlock : implement design for in-app unlock
This commit is contained in:
@@ -94,7 +94,8 @@ class LockScreenFlowNode @AssistedInject constructor(
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Unlock -> {
|
||||
createNode<PinUnlockNode>(buildContext)
|
||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
|
||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
|
||||
}
|
||||
NavTarget.Setup -> {
|
||||
createNode<LockScreenSetupFlowNode>(buildContext)
|
||||
|
||||
@@ -113,7 +113,8 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Unlock -> {
|
||||
createNode<PinUnlockNode>(buildContext)
|
||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
|
||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
|
||||
}
|
||||
NavTarget.Setup -> {
|
||||
val callback = object : LockScreenSetupFlowNode.Callback {
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
|
||||
sealed interface PinUnlockEvents {
|
||||
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
|
||||
data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvents
|
||||
data object OnForgetPin : PinUnlockEvents
|
||||
data object ClearSignOutPrompt : PinUnlockEvents
|
||||
data object SignOut : PinUnlockEvents
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -33,11 +35,18 @@ class PinUnlockNode @AssistedInject constructor(
|
||||
private val presenter: PinUnlockPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val isInAppUnlock: Boolean
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = inputs.isInAppUnlock,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ class PinUnlockPresenter @Inject constructor(
|
||||
PinUnlockEvents.ClearBiometricError -> {
|
||||
biometricUnlockResult = null
|
||||
}
|
||||
is PinUnlockEvents.OnPinEntryChanged -> {
|
||||
pinEntryState.value = pinEntry.process(event.entryAsText)
|
||||
}
|
||||
}
|
||||
}
|
||||
return PinUnlockState(
|
||||
@@ -159,6 +162,16 @@ class PinUnlockPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.process(pinEntryAsText: String): Async<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> {
|
||||
val pinEntry = data.fillWith(pinEntryAsText)
|
||||
Async.Success(pinEntry)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError = true)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
@@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
@@ -37,8 +39,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -46,10 +52,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.element.android.features.lockscreen.impl.R
|
||||
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
|
||||
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.keypad.PinKeypad
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
@@ -66,6 +74,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
fun PinUnlockView(
|
||||
state: PinUnlockState,
|
||||
isInAppUnlock: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
@@ -75,56 +84,7 @@ fun PinUnlockView(
|
||||
}
|
||||
}
|
||||
Surface(modifier) {
|
||||
BoxWithConstraints {
|
||||
val commonModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.padding(all = 20.dp)
|
||||
|
||||
val header = @Composable {
|
||||
PinUnlockHeader(
|
||||
state = state,
|
||||
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
val footer = @Composable {
|
||||
PinUnlockFooter(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
showBiometricUnlock = state.showBiometricUnlock,
|
||||
onUseBiometric = {
|
||||
state.eventSink(PinUnlockEvents.OnUseBiometric)
|
||||
},
|
||||
onForgotPin = {
|
||||
state.eventSink(PinUnlockEvents.OnForgetPin)
|
||||
},
|
||||
)
|
||||
}
|
||||
val content = @Composable { constraints: BoxWithConstraintsScope ->
|
||||
PinKeypad(
|
||||
onClick = {
|
||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
|
||||
},
|
||||
maxWidth = constraints.maxWidth,
|
||||
maxHeight = constraints.maxHeight,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
)
|
||||
}
|
||||
if (maxHeight < 600.dp) {
|
||||
PinUnlockCompactView(
|
||||
header = header,
|
||||
footer = footer,
|
||||
content = content,
|
||||
modifier = commonModifier,
|
||||
)
|
||||
} else {
|
||||
PinUnlockExpandedView(
|
||||
header = header,
|
||||
footer = footer,
|
||||
content = content,
|
||||
modifier = commonModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock)
|
||||
if (state.showSignOutPrompt) {
|
||||
SignOutPrompt(
|
||||
isCancellable = state.isSignOutPromptCancellable,
|
||||
@@ -144,6 +104,86 @@ fun PinUnlockView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinUnlockPage(
|
||||
state: PinUnlockState,
|
||||
isInAppUnlock: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
val commonModifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding()
|
||||
.padding(all = 20.dp)
|
||||
|
||||
val header = @Composable {
|
||||
PinUnlockHeader(
|
||||
state = state,
|
||||
isInAppUnlock = isInAppUnlock,
|
||||
modifier = Modifier.padding(top = 60.dp)
|
||||
)
|
||||
}
|
||||
val footer = @Composable {
|
||||
PinUnlockFooter(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
showBiometricUnlock = state.showBiometricUnlock,
|
||||
onUseBiometric = {
|
||||
state.eventSink(PinUnlockEvents.OnUseBiometric)
|
||||
},
|
||||
onForgotPin = {
|
||||
state.eventSink(PinUnlockEvents.OnForgetPin)
|
||||
},
|
||||
)
|
||||
}
|
||||
val content = @Composable { constraints: BoxWithConstraintsScope ->
|
||||
if (isInAppUnlock) {
|
||||
val pinEntry = state.pinEntry.dataOrNull()
|
||||
if (pinEntry != null) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
PinEntryTextField(
|
||||
pinEntry = pinEntry,
|
||||
isSecured = true,
|
||||
onValueChange = {
|
||||
state.eventSink(PinUnlockEvents.OnPinEntryChanged(it))
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PinKeypad(
|
||||
onClick = {
|
||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
|
||||
},
|
||||
maxWidth = constraints.maxWidth,
|
||||
maxHeight = constraints.maxHeight,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (maxHeight < 600.dp) {
|
||||
PinUnlockCompactView(
|
||||
header = header,
|
||||
footer = footer,
|
||||
content = content,
|
||||
modifier = commonModifier,
|
||||
)
|
||||
} else {
|
||||
PinUnlockExpandedView(
|
||||
header = header,
|
||||
footer = footer,
|
||||
content = content,
|
||||
modifier = commonModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignOutPrompt(
|
||||
isCancellable: Boolean,
|
||||
@@ -248,16 +288,21 @@ private fun PinDot(
|
||||
@Composable
|
||||
private fun PinUnlockHeader(
|
||||
state: PinUnlockState,
|
||||
isInAppUnlock: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp),
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = "",
|
||||
)
|
||||
if (isInAppUnlock) {
|
||||
RoundedIconAtom(imageVector = Icons.Filled.Lock)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp),
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.common_enter_your_pin),
|
||||
@@ -290,8 +335,8 @@ private fun PinUnlockHeader(
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = subtitleColor,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
if (state.pinEntry is Async.Success) {
|
||||
if (!isInAppUnlock && state.pinEntry is Async.Success) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
PinDotsRow(state.pinEntry.data)
|
||||
}
|
||||
}
|
||||
@@ -314,10 +359,22 @@ private fun PinUnlockFooter(
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||
internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||
ElementPreview {
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun PinUnlockDefaultViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||
ElementPreview {
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user