Skip to content

Commit 46869b8

Browse files
authored
Merge pull request #1714 from vector-im/feature/fga/unlock_settings_2
Pin unlock : implement design for in-app unlock
2 parents 6832b1f + d1a48bf commit 46869b8

File tree

50 files changed

+228
-104
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+228
-104
lines changed

features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ class LockScreenFlowNode @AssistedInject constructor(
9494
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
9595
return when (navTarget) {
9696
NavTarget.Unlock -> {
97-
createNode<PinUnlockNode>(buildContext)
97+
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
98+
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
9899
}
99100
NavTarget.Setup -> {
100101
createNode<LockScreenSetupFlowNode>(buildContext)

features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
113113
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
114114
return when (navTarget) {
115115
NavTarget.Unlock -> {
116-
createNode<PinUnlockNode>(buildContext)
116+
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
117+
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
117118
}
118119
NavTarget.Setup -> {
119120
val callback = object : LockScreenSetupFlowNode.Callback {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
2020

2121
sealed interface PinUnlockEvents {
2222
data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents
23+
data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvents
2324
data object OnForgetPin : PinUnlockEvents
2425
data object ClearSignOutPrompt : PinUnlockEvents
2526
data object SignOut : PinUnlockEvents

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import com.bumble.appyx.core.plugin.Plugin
2424
import dagger.assisted.Assisted
2525
import dagger.assisted.AssistedInject
2626
import io.element.android.anvilannotations.ContributesNode
27+
import io.element.android.libraries.architecture.NodeInputs
28+
import io.element.android.libraries.architecture.inputs
2729
import io.element.android.libraries.di.SessionScope
2830

2931
@ContributesNode(SessionScope::class)
@@ -33,11 +35,18 @@ class PinUnlockNode @AssistedInject constructor(
3335
private val presenter: PinUnlockPresenter,
3436
) : Node(buildContext, plugins = plugins) {
3537

38+
data class Inputs(
39+
val isInAppUnlock: Boolean
40+
) : NodeInputs
41+
42+
private val inputs: Inputs = inputs()
43+
3644
@Composable
3745
override fun View(modifier: Modifier) {
3846
val state = presenter.present()
3947
PinUnlockView(
4048
state = state,
49+
isInAppUnlock = inputs.isInAppUnlock,
4150
modifier = modifier
4251
)
4352
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ class PinUnlockPresenter @Inject constructor(
116116
PinUnlockEvents.ClearBiometricError -> {
117117
biometricUnlockResult = null
118118
}
119+
is PinUnlockEvents.OnPinEntryChanged -> {
120+
pinEntryState.value = pinEntry.process(event.entryAsText)
121+
}
119122
}
120123
}
121124
return PinUnlockState(
@@ -159,6 +162,16 @@ class PinUnlockPresenter @Inject constructor(
159162
}
160163
}
161164

165+
private fun Async<PinEntry>.process(pinEntryAsText: String): Async<PinEntry> {
166+
return when (this) {
167+
is Async.Success -> {
168+
val pinEntry = data.fillWith(pinEntryAsText)
169+
Async.Success(pinEntry)
170+
}
171+
else -> this
172+
}
173+
}
174+
162175
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
163176
suspend {
164177
matrixClient.logout(ignoreSdkError = true)

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

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
1718
package io.element.android.features.lockscreen.impl.unlock
1819

1920
import androidx.compose.foundation.background
@@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
2930
import androidx.compose.foundation.layout.fillMaxSize
3031
import androidx.compose.foundation.layout.fillMaxWidth
3132
import androidx.compose.foundation.layout.height
33+
import androidx.compose.foundation.layout.imePadding
3234
import androidx.compose.foundation.layout.padding
3335
import androidx.compose.foundation.layout.size
3436
import androidx.compose.foundation.layout.systemBarsPadding
@@ -37,19 +39,25 @@ import androidx.compose.material.icons.Icons
3739
import androidx.compose.material.icons.filled.Lock
3840
import androidx.compose.material3.MaterialTheme
3941
import androidx.compose.runtime.Composable
42+
import androidx.compose.runtime.LaunchedEffect
43+
import androidx.compose.runtime.remember
4044
import androidx.compose.ui.Alignment
4145
import androidx.compose.ui.Modifier
46+
import androidx.compose.ui.focus.FocusRequester
47+
import androidx.compose.ui.focus.focusRequester
4248
import androidx.compose.ui.res.pluralStringResource
4349
import androidx.compose.ui.res.stringResource
4450
import androidx.compose.ui.text.style.TextAlign
4551
import androidx.compose.ui.tooling.preview.PreviewParameter
4652
import androidx.compose.ui.unit.dp
4753
import androidx.lifecycle.Lifecycle
4854
import io.element.android.features.lockscreen.impl.R
55+
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
4956
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
5057
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
5158
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
5259
import io.element.android.libraries.architecture.Async
60+
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
5361
import io.element.android.libraries.designsystem.components.ProgressDialog
5462
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
5563
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@@ -66,6 +74,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
6674
@Composable
6775
fun PinUnlockView(
6876
state: PinUnlockState,
77+
isInAppUnlock: Boolean,
6978
modifier: Modifier = Modifier,
7079
) {
7180
OnLifecycleEvent { _, event ->
@@ -75,56 +84,7 @@ fun PinUnlockView(
7584
}
7685
}
7786
Surface(modifier) {
78-
BoxWithConstraints {
79-
val commonModifier = Modifier
80-
.fillMaxSize()
81-
.systemBarsPadding()
82-
.padding(all = 20.dp)
83-
84-
val header = @Composable {
85-
PinUnlockHeader(
86-
state = state,
87-
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)
88-
)
89-
}
90-
val footer = @Composable {
91-
PinUnlockFooter(
92-
modifier = Modifier.padding(top = 24.dp),
93-
showBiometricUnlock = state.showBiometricUnlock,
94-
onUseBiometric = {
95-
state.eventSink(PinUnlockEvents.OnUseBiometric)
96-
},
97-
onForgotPin = {
98-
state.eventSink(PinUnlockEvents.OnForgetPin)
99-
},
100-
)
101-
}
102-
val content = @Composable { constraints: BoxWithConstraintsScope ->
103-
PinKeypad(
104-
onClick = {
105-
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
106-
},
107-
maxWidth = constraints.maxWidth,
108-
maxHeight = constraints.maxHeight,
109-
horizontalAlignment = Alignment.CenterHorizontally,
110-
)
111-
}
112-
if (maxHeight < 600.dp) {
113-
PinUnlockCompactView(
114-
header = header,
115-
footer = footer,
116-
content = content,
117-
modifier = commonModifier,
118-
)
119-
} else {
120-
PinUnlockExpandedView(
121-
header = header,
122-
footer = footer,
123-
content = content,
124-
modifier = commonModifier,
125-
)
126-
}
127-
}
87+
PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock)
12888
if (state.showSignOutPrompt) {
12989
SignOutPrompt(
13090
isCancellable = state.isSignOutPromptCancellable,
@@ -144,6 +104,86 @@ fun PinUnlockView(
144104
}
145105
}
146106

107+
@Composable
108+
private fun PinUnlockPage(
109+
state: PinUnlockState,
110+
isInAppUnlock: Boolean,
111+
modifier: Modifier = Modifier
112+
) {
113+
BoxWithConstraints {
114+
val commonModifier = modifier
115+
.fillMaxSize()
116+
.systemBarsPadding()
117+
.imePadding()
118+
.padding(all = 20.dp)
119+
120+
val header = @Composable {
121+
PinUnlockHeader(
122+
state = state,
123+
isInAppUnlock = isInAppUnlock,
124+
modifier = Modifier.padding(top = 60.dp)
125+
)
126+
}
127+
val footer = @Composable {
128+
PinUnlockFooter(
129+
modifier = Modifier.padding(top = 24.dp),
130+
showBiometricUnlock = state.showBiometricUnlock,
131+
onUseBiometric = {
132+
state.eventSink(PinUnlockEvents.OnUseBiometric)
133+
},
134+
onForgotPin = {
135+
state.eventSink(PinUnlockEvents.OnForgetPin)
136+
},
137+
)
138+
}
139+
val content = @Composable { constraints: BoxWithConstraintsScope ->
140+
if (isInAppUnlock) {
141+
val pinEntry = state.pinEntry.dataOrNull()
142+
if (pinEntry != null) {
143+
val focusRequester = remember { FocusRequester() }
144+
LaunchedEffect(Unit) {
145+
focusRequester.requestFocus()
146+
}
147+
PinEntryTextField(
148+
pinEntry = pinEntry,
149+
isSecured = true,
150+
onValueChange = {
151+
state.eventSink(PinUnlockEvents.OnPinEntryChanged(it))
152+
},
153+
modifier = Modifier
154+
.focusRequester(focusRequester)
155+
.fillMaxWidth()
156+
)
157+
}
158+
} else {
159+
PinKeypad(
160+
onClick = {
161+
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it))
162+
},
163+
maxWidth = constraints.maxWidth,
164+
maxHeight = constraints.maxHeight,
165+
horizontalAlignment = Alignment.CenterHorizontally,
166+
)
167+
}
168+
}
169+
if (maxHeight < 600.dp) {
170+
PinUnlockCompactView(
171+
header = header,
172+
footer = footer,
173+
content = content,
174+
modifier = commonModifier,
175+
)
176+
} else {
177+
PinUnlockExpandedView(
178+
header = header,
179+
footer = footer,
180+
content = content,
181+
modifier = commonModifier,
182+
)
183+
}
184+
}
185+
}
186+
147187
@Composable
148188
private fun SignOutPrompt(
149189
isCancellable: Boolean,
@@ -248,16 +288,21 @@ private fun PinDot(
248288
@Composable
249289
private fun PinUnlockHeader(
250290
state: PinUnlockState,
291+
isInAppUnlock: Boolean,
251292
modifier: Modifier = Modifier,
252293
) {
253294
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
254-
Icon(
255-
modifier = Modifier
256-
.size(32.dp),
257-
tint = ElementTheme.colors.iconPrimary,
258-
imageVector = Icons.Filled.Lock,
259-
contentDescription = "",
260-
)
295+
if (isInAppUnlock) {
296+
RoundedIconAtom(imageVector = Icons.Filled.Lock)
297+
} else {
298+
Icon(
299+
modifier = Modifier
300+
.size(32.dp),
301+
tint = ElementTheme.colors.iconPrimary,
302+
imageVector = Icons.Filled.Lock,
303+
contentDescription = "",
304+
)
305+
}
261306
Spacer(modifier = Modifier.height(16.dp))
262307
Text(
263308
text = stringResource(id = CommonStrings.common_enter_your_pin),
@@ -290,8 +335,8 @@ private fun PinUnlockHeader(
290335
style = ElementTheme.typography.fontBodyMdRegular,
291336
color = subtitleColor,
292337
)
293-
Spacer(Modifier.height(24.dp))
294-
if (state.pinEntry is Async.Success) {
338+
if (!isInAppUnlock && state.pinEntry is Async.Success) {
339+
Spacer(Modifier.height(24.dp))
295340
PinDotsRow(state.pinEntry.data)
296341
}
297342
}
@@ -314,10 +359,22 @@ private fun PinUnlockFooter(
314359

315360
@Composable
316361
@PreviewsDayNight
317-
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
362+
internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
363+
ElementPreview {
364+
PinUnlockView(
365+
state = state,
366+
isInAppUnlock = true,
367+
)
368+
}
369+
}
370+
371+
@Composable
372+
@PreviewsDayNight
373+
internal fun PinUnlockDefaultViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
318374
ElementPreview {
319375
PinUnlockView(
320376
state = state,
377+
isInAppUnlock = false,
321378
)
322379
}
323380
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)