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:
ganfra 2023-10-31 21:39:25 +01:00 committed by GitHub
commit 9363e3ffd6
50 changed files with 228 additions and 104 deletions

View file

@ -94,7 +94,8 @@ class LockScreenFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Unlock -> { NavTarget.Unlock -> {
createNode<PinUnlockNode>(buildContext) val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
} }
NavTarget.Setup -> { NavTarget.Setup -> {
createNode<LockScreenSetupFlowNode>(buildContext) createNode<LockScreenSetupFlowNode>(buildContext)

View file

@ -113,7 +113,8 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Unlock -> { NavTarget.Unlock -> {
createNode<PinUnlockNode>(buildContext) val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
} }
NavTarget.Setup -> { NavTarget.Setup -> {
val callback = object : LockScreenSetupFlowNode.Callback { val callback = object : LockScreenSetupFlowNode.Callback {

View file

@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
sealed interface PinUnlockEvents { sealed interface PinUnlockEvents {
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvents
data object OnForgetPin : PinUnlockEvents data object OnForgetPin : PinUnlockEvents
data object ClearSignOutPrompt : PinUnlockEvents data object ClearSignOutPrompt : PinUnlockEvents
data object SignOut : PinUnlockEvents data object SignOut : PinUnlockEvents

View file

@ -24,6 +24,8 @@ import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode 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 import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -33,11 +35,18 @@ class PinUnlockNode @AssistedInject constructor(
private val presenter: PinUnlockPresenter, private val presenter: PinUnlockPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
data class Inputs(
val isInAppUnlock: Boolean
) : NodeInputs
private val inputs: Inputs = inputs()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
PinUnlockView( PinUnlockView(
state = state, state = state,
isInAppUnlock = inputs.isInAppUnlock,
modifier = modifier modifier = modifier
) )
} }

View file

@ -116,6 +116,9 @@ class PinUnlockPresenter @Inject constructor(
PinUnlockEvents.ClearBiometricError -> { PinUnlockEvents.ClearBiometricError -> {
biometricUnlockResult = null biometricUnlockResult = null
} }
is PinUnlockEvents.OnPinEntryChanged -> {
pinEntryState.value = pinEntry.process(event.entryAsText)
}
} }
} }
return PinUnlockState( 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 { private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
suspend { suspend {
matrixClient.logout(ignoreSdkError = true) matrixClient.logout(ignoreSdkError = true)

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.foundation.background 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding 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.material.icons.filled.Lock
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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
@ -46,10 +52,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import io.element.android.features.lockscreen.impl.R 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.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.keypad.PinKeypad import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
import io.element.android.libraries.architecture.Async 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.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -66,6 +74,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun PinUnlockView( fun PinUnlockView(
state: PinUnlockState, state: PinUnlockState,
isInAppUnlock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
OnLifecycleEvent { _, event -> OnLifecycleEvent { _, event ->
@ -75,56 +84,7 @@ fun PinUnlockView(
} }
} }
Surface(modifier) { Surface(modifier) {
BoxWithConstraints { PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock)
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,
)
}
}
if (state.showSignOutPrompt) { if (state.showSignOutPrompt) {
SignOutPrompt( SignOutPrompt(
isCancellable = state.isSignOutPromptCancellable, 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 @Composable
private fun SignOutPrompt( private fun SignOutPrompt(
isCancellable: Boolean, isCancellable: Boolean,
@ -248,16 +288,21 @@ private fun PinDot(
@Composable @Composable
private fun PinUnlockHeader( private fun PinUnlockHeader(
state: PinUnlockState, state: PinUnlockState,
isInAppUnlock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Icon( if (isInAppUnlock) {
modifier = Modifier RoundedIconAtom(imageVector = Icons.Filled.Lock)
.size(32.dp), } else {
tint = ElementTheme.colors.iconPrimary, Icon(
imageVector = Icons.Filled.Lock, modifier = Modifier
contentDescription = "", .size(32.dp),
) tint = ElementTheme.colors.iconPrimary,
imageVector = Icons.Filled.Lock,
contentDescription = "",
)
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = CommonStrings.common_enter_your_pin), text = stringResource(id = CommonStrings.common_enter_your_pin),
@ -290,8 +335,8 @@ private fun PinUnlockHeader(
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = subtitleColor, color = subtitleColor,
) )
Spacer(Modifier.height(24.dp)) if (!isInAppUnlock && state.pinEntry is Async.Success) {
if (state.pinEntry is Async.Success) { Spacer(Modifier.height(24.dp))
PinDotsRow(state.pinEntry.data) PinDotsRow(state.pinEntry.data)
} }
} }
@ -314,10 +359,22 @@ private fun PinUnlockFooter(
@Composable @Composable
@PreviewsDayNight @PreviewsDayNight
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
ElementPreview { ElementPreview {
PinUnlockView( PinUnlockView(
state = state, state = state,
isInAppUnlock = true,
)
}
}
@Composable
@PreviewsDayNight
internal fun PinUnlockDefaultViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
ElementPreview {
PinUnlockView(
state = state,
isInAppUnlock = false,
) )
} }
} }

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1ece6fcfbf1c064287f6e0ead16c3abc471ea3f7db1aa4dbb7b8cdd2657e115a
size 40369

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8526a23e31e2c87ca2f43b127f00f05067a3525eff3319fe2965530096caab87
size 40836

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88a97cfbbb601a889cc1f2a81ac6fcb9a58288232358968d536f4216ae8882f0
size 41692

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5313634888c4d26b2a6c92485de0b2b4d7a6479ba5a872794c9fa8d8d30639b
size 46796

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82d010d152311c8a19e33fa80ea7d70c061a0ed3bdc654f0cb7276207398d4ae
size 37930

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee09e25c131831cb08ebd85fb2fcbe078f075da74734f34c9811fac6b6e30437
size 44372

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38dfb87d845af20021622440ff3fee5d81f32d1a8a574f4ab9fece71ce36f5ce
size 35193

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d538426d487708ba2139b322b31ff58675279f19c9dec53d636f68b0d5fa6a0
size 37231

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9651646be4e8b732d9d8e1395191ac2b3e1ca2dbf7280090741ffcac290402d4
size 37679

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:079e0f74c91d988d03af37ff66a94ad85df5e6312d7ebd531fde1cbf1105ec8e
size 38219

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:724b77f4eb0e3d721b05eddfc3d9c2e5b585213d1a4e4eca5d598175536abe47
size 41018

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6236756ef44cf19160e7977a9390dee51950f5324446355179e16285c5584270
size 35185

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd70746d6c331e0c2004bc756b8ef207f755b357b3a6eff57ad085ff75e08a0f
size 38676

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4faa194d51d356d8ad659c225a92b124d9d93f16972acff1aa1b45aabfd101b6
size 31475

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b58f216453eb1d131ece67219021f074c1f5b1bc7593b76a2efedfb2c9f09887
size 23690

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65226593d4548b87ded518f143b9a2030908befc5f423846a03e7938fe681390
size 23410

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41e9a94fd494477437bbb625df08870875ef8021f7b02bc6e1953fac0cd3a8d3
size 24948

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9c4bc23a4a15ebc6d434da33381336f01d7c1566fb9ef9dae94bf03138df334
size 39615

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7081eb4ef25d5220e638e83ad4a3aa4248db8ddf22e371cb7698b6ce0fd91f3
size 21328

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f7aa0b84dd7b059443afb2d0f281ab684266883010e9146b557954bf17c7807
size 37078

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96299aa2059245aa3dd2c374d122c8cbda50040bf80930e62e74d634407bc913
size 26731

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:900a9ce415198cb4736d99f91e1f997021363f45ab5dded5e3870631e91c864f
size 22476

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bdb6f9f7e684d0bf2f84670cc7848b038fd6c8f7fde119de53fcc576aa522af5
size 22420

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9428610c1ffa0b93e53d1470a06e3cc8f3568a2dd86fd3fa99ef76d935220a5d
size 23467

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05f512aa7541a7b5f92f14c3f9bcc2c8d69141b92fd0be4fcc2ead162ccd7015
size 34474

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02876aee2a4e9574d4503bb2cb71ff25a588e5917356cff3f3d0ac517a16eee3
size 20399

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae7c91ff420b979f5b818e5f750e3c740c9c03a0301660f90bb7471997c88b6b
size 32104

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4bca2311722ab2f3603da2cdb6181411c83b929c38b87b63a8923d0e563f915a
size 23809

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c94f10441359bdb901ee1f8be9c307312586d6ca54495e88ec231c1e931eb36b
size 38994

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38456a511fbc72e3d544c8f88138be4f24b6adb3a10629989c037141213a9e19
size 39441

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:083c3972e5d019ce1d8565a655bd6f6fb6689e17792ddf7ed282d5eee529a7b6
size 40292

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bda49a721fbd19b4cbe0f4dc1b4013154a55e17f78e099f411d1caaef82cfa6c
size 46711

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1facbdcba634b37a8162876e6af02a3565eaee2ac4afe32816b1f8e9602b2a65
size 36565

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0d360c0697733a9e1067589be5178f631da673743b97daeb50ebdc51fd89502
size 44289

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc653f37c55ce7f9ef001d8f8ab57de23008baae2f644c6db49a7f4bccc831d5
size 34938

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab8c04689414b5183fa6ab5a479063b478b3d02dfb6bf2df72b1b606c77392ad
size 36278

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fda961a2240c1458ad561c8f4e7fa265bbe6c70cf9b0fbfcd1ae0879036a3343
size 36714

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cb889338b21dd8e1a913348ff02cd217f48840c4d54b99ed4d89b67dff771bb
size 37254

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:836f26b162a031b14151511405ec0b9dd43fe1c37127a5ec05e277ab0e2ab667
size 41054

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4b3e856f102b2d53fd5776204a8576ed515e9e4bfd9899e4138eb1515a11b12
size 34241

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c53928f35405266f99bbc4976f9c12915f47765289481b2cd65086925ea1b50
size 38710

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd06f078cff08947537a58ddc2df9a79f4aa3d55372b6b8eafa962f2266ca02f
size 31443