Skip to content

Commit 31a952c

Browse files
jmartinespandybalaamElementBot
authored
Hide the recovery key while we are entering it (#5147)
* Hide the recovery key while we are entering it (#5134) This is the Element X Android part of element-hq/element-meta#2888 * Move the textfield contents being visible to the state so we can preview and test it * Always use the password visual transformation for the recovery key field * Update screenshots --------- Co-authored-by: Andy Balaam <[email protected]> Co-authored-by: ElementBot <[email protected]>
1 parent a1c36d9 commit 31a952c

File tree

35 files changed

+138
-42
lines changed

35 files changed

+138
-42
lines changed

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.securebackup.impl.enter
99

1010
sealed interface SecureBackupEnterRecoveryKeyEvents {
1111
data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvents
12+
data class ChangeRecoveryKeyFieldContentsVisibility(val visible: Boolean) : SecureBackupEnterRecoveryKeyEvents
1213
data object Submit : SecureBackupEnterRecoveryKeyEvents
1314
data object ClearDialog : SecureBackupEnterRecoveryKeyEvents
1415
}

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
3333
@Composable
3434
override fun present(): SecureBackupEnterRecoveryKeyState {
3535
val coroutineScope = rememberCoroutineScope()
36+
var displayRecoveryKeyFieldContents by rememberSaveable {
37+
mutableStateOf(false)
38+
}
3639
var recoveryKey by rememberSaveable {
3740
mutableStateOf("")
3841
}
@@ -59,13 +62,17 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
5962
// No need to remove the spaces, the SDK will do it.
6063
coroutineScope.submitRecoveryKey(recoveryKey, submitAction)
6164
}
65+
is SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility -> {
66+
displayRecoveryKeyFieldContents = event.visible
67+
}
6268
}
6369
}
6470

6571
return SecureBackupEnterRecoveryKeyState(
6672
recoveryKeyViewState = RecoveryKeyViewState(
6773
recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
6874
formattedRecoveryKey = recoveryKey,
75+
displayTextFieldContents = displayRecoveryKeyFieldContents,
6976
inProgress = submitAction.value.isLoading(),
7077
),
7178
isSubmitEnabled = recoveryKey.isNotEmpty() && submitAction.value.isUninitialized(),

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,21 @@ open class SecureBackupEnterRecoveryKeyStateProvider : PreviewParameterProvider<
2020
aSecureBackupEnterRecoveryKeyState(),
2121
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Loading),
2222
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Failure(Exception("A Failure"))),
23+
aSecureBackupEnterRecoveryKeyState(displayTextFieldContents = false),
2324
)
2425
}
2526

2627
fun aSecureBackupEnterRecoveryKeyState(
2728
recoveryKey: String = aFormattedRecoveryKey(),
2829
isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(),
30+
displayTextFieldContents: Boolean = true,
2931
submitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
3032
eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit = {},
3133
) = SecureBackupEnterRecoveryKeyState(
3234
recoveryKeyViewState = RecoveryKeyViewState(
3335
recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
3436
formattedRecoveryKey = recoveryKey,
37+
displayTextFieldContents = displayTextFieldContents,
3538
inProgress = submitAction.isLoading(),
3639
),
3740
isSubmitEnabled = isSubmitEnabled,

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ private fun Content(
102102
onSubmit = {
103103
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
104104
},
105+
toggleRecoveryKeyVisibility = {
106+
state.eventSink(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(it))
107+
}
105108
)
106109
}
107110

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class SecureBackupSetupPresenter @AssistedInject constructor(
7171
val recoveryKeyViewState = RecoveryKeyViewState(
7272
recoveryKeyUserStory = if (isChangeRecoveryKeyUserStory) RecoveryKeyUserStory.Change else RecoveryKeyUserStory.Setup,
7373
formattedRecoveryKey = setupState.recoveryKey(),
74+
displayTextFieldContents = true,
7475
inProgress = setupState is SetupState.Creating,
7576
)
7677

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ private fun SetupState.toRecoveryKeyViewState(): RecoveryKeyViewState {
4343
return RecoveryKeyViewState(
4444
recoveryKeyUserStory = RecoveryKeyUserStory.Setup,
4545
formattedRecoveryKey = recoveryKey(),
46+
displayTextFieldContents = true,
4647
inProgress = this is SetupState.Creating,
4748
)
4849
}

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ private fun Content(
138138
onClick = clickLambda,
139139
onChange = null,
140140
onSubmit = null,
141+
toggleRecoveryKeyVisibility = {},
141142
)
142143
}
143144

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.securebackup.impl.setup.views
99

1010
import androidx.compose.foundation.background
11+
import androidx.compose.foundation.clickable
1112
import androidx.compose.foundation.layout.Arrangement
1213
import androidx.compose.foundation.layout.Box
1314
import androidx.compose.foundation.layout.Column
@@ -20,7 +21,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
2021
import androidx.compose.foundation.text.KeyboardActions
2122
import androidx.compose.foundation.text.KeyboardOptions
2223
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.getValue
2325
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.setValue
2427
import androidx.compose.ui.Alignment
2528
import androidx.compose.ui.Modifier
2629
import androidx.compose.ui.autofill.ContentType
@@ -32,6 +35,7 @@ import androidx.compose.ui.semantics.semantics
3235
import androidx.compose.ui.text.font.FontFamily
3336
import androidx.compose.ui.text.input.ImeAction
3437
import androidx.compose.ui.text.input.KeyboardType
38+
import androidx.compose.ui.text.input.PasswordVisualTransformation
3539
import androidx.compose.ui.text.input.VisualTransformation
3640
import androidx.compose.ui.text.style.TextAlign
3741
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -57,6 +61,7 @@ internal fun RecoveryKeyView(
5761
onClick: (() -> Unit)?,
5862
onChange: ((String) -> Unit)?,
5963
onSubmit: (() -> Unit)?,
64+
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
6065
modifier: Modifier = Modifier,
6166
) {
6267
Column(
@@ -67,7 +72,7 @@ internal fun RecoveryKeyView(
6772
text = stringResource(id = CommonStrings.common_recovery_key),
6873
style = ElementTheme.typography.fontBodyMdRegular,
6974
)
70-
RecoveryKeyContent(state, onClick, onChange, onSubmit)
75+
RecoveryKeyContent(state, onClick, onChange, onSubmit, toggleRecoveryKeyVisibility)
7176
RecoveryKeyFooter(state)
7277
}
7378
}
@@ -78,11 +83,17 @@ private fun RecoveryKeyContent(
7883
onClick: (() -> Unit)?,
7984
onChange: ((String) -> Unit)?,
8085
onSubmit: (() -> Unit)?,
86+
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
8187
) {
8288
when (state.recoveryKeyUserStory) {
8389
RecoveryKeyUserStory.Setup,
8490
RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick)
85-
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange, onSubmit)
91+
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(
92+
state = state,
93+
toggleRecoveryKeyVisibility = toggleRecoveryKeyVisibility,
94+
onChange = onChange,
95+
onSubmit = onSubmit,
96+
)
8697
}
8798
}
8899

@@ -171,15 +182,24 @@ private fun RecoveryKeyWithCopy(
171182
@Composable
172183
private fun RecoveryKeyFormContent(
173184
state: RecoveryKeyViewState,
185+
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
174186
onChange: ((String) -> Unit)?,
175187
onSubmit: (() -> Unit)?,
176188
) {
177189
onChange ?: error("onChange should not be null")
178190
onSubmit ?: error("onSubmit should not be null")
191+
if (state.inProgress) {
192+
// Ensure recovery key is hidden when user submits the form
193+
toggleRecoveryKeyVisibility(false)
194+
}
179195
val keyHasSpace = state.formattedRecoveryKey.orEmpty().contains(" ")
180-
val recoveryKeyVisualTransformation = remember(keyHasSpace) {
181-
// Do not apply a visual transformation if the key has spaces, to let user enter passphrase
182-
if (keyHasSpace) VisualTransformation.None else RecoveryKeyVisualTransformation()
196+
val recoveryKeyVisualTransformation = remember(keyHasSpace, state.displayTextFieldContents) {
197+
if (state.displayTextFieldContents) {
198+
// Do not apply a visual transformation if the key has spaces, to let user enter passphrase
199+
if (keyHasSpace) VisualTransformation.None else RecoveryKeyVisualTransformation()
200+
} else {
201+
PasswordVisualTransformation()
202+
}
183203
}
184204
TextField(
185205
modifier = Modifier
@@ -201,6 +221,18 @@ private fun RecoveryKeyFormContent(
201221
onDone = { onSubmit() }
202222
),
203223
placeholder = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder),
224+
trailingIcon = {
225+
val image =
226+
if (state.displayTextFieldContents) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
227+
val description =
228+
if (state.displayTextFieldContents) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
229+
Box(Modifier.clickable { toggleRecoveryKeyVisibility(!state.displayTextFieldContents) }) {
230+
Icon(
231+
imageVector = image,
232+
contentDescription = description,
233+
)
234+
}
235+
},
204236
)
205237
}
206238

@@ -249,5 +281,6 @@ internal fun RecoveryKeyViewPreview(
249281
onClick = {},
250282
onChange = {},
251283
onSubmit = {},
284+
toggleRecoveryKeyVisibility = {},
252285
)
253286
}

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package io.element.android.features.securebackup.impl.setup.views
1010
data class RecoveryKeyViewState(
1111
val recoveryKeyUserStory: RecoveryKeyUserStory,
1212
val formattedRecoveryKey: String?,
13+
val displayTextFieldContents: Boolean,
1314
val inProgress: Boolean,
1415
)
1516

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,23 @@ open class RecoveryKeyViewStateProvider : PreviewParameterProvider<RecoveryKeyVi
2222
} + sequenceOf(
2323
aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = aFormattedRecoveryKey().replace(" ", "")),
2424
aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = "This is a passphrase with spaces"),
25+
aRecoveryKeyViewState(
26+
recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
27+
formattedRecoveryKey = aFormattedRecoveryKey().replace(" ", ""),
28+
displayTextFieldContents = false
29+
),
2530
)
2631
}
2732

2833
fun aRecoveryKeyViewState(
2934
recoveryKeyUserStory: RecoveryKeyUserStory = RecoveryKeyUserStory.Setup,
3035
formattedRecoveryKey: String? = null,
3136
inProgress: Boolean = false,
37+
displayTextFieldContents: Boolean = true,
3238
) = RecoveryKeyViewState(
3339
recoveryKeyUserStory = recoveryKeyUserStory,
3440
formattedRecoveryKey = formattedRecoveryKey,
41+
displayTextFieldContents = displayTextFieldContents,
3542
inProgress = inProgress,
3643
)
3744

0 commit comments

Comments
 (0)