Always display 'lost recovery key?' option (#2745)

* Always display 'lost recovery key?' option

* Use `isLastDevice` it to display only 'enter recovery key' option for verification.

* Update strings. This should fix the wrong term 'passcode' being used in the recovery key screen title.

* Disable 'lost your recovery key?' button while the screen is in a loading state

* Update screenshots

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-04-25 15:28:24 +02:00 committed by GitHub
parent 6219190ef1
commit 7397df806b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 181 additions and 83 deletions

View file

@ -23,6 +23,12 @@ plugins {
android {
namespace = "io.element.android.features.securebackup.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -51,8 +57,11 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor)
}

View file

@ -136,6 +136,10 @@ class SecureBackupFlowNode @AssistedInject constructor(
backstack.pop()
}
}
override fun onCreateNewRecoveryKey() {
backstack.push(NavTarget.CreateNewRecoveryKey)
}
}
createNode<SecureBackupEnterRecoveryKeyNode>(buildContext, plugins = listOf(callback))
}

View file

@ -35,6 +35,7 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onEnterRecoveryKeySuccess()
fun onCreateNewRecoveryKey()
}
private val callback = plugins<Callback>().first()
@ -47,6 +48,7 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
modifier = modifier,
onDone = callback::onEnterRecoveryKeySuccess,
onBackClicked = ::navigateUp,
onCreateNewRecoveryKey = callback::onCreateNewRecoveryKey
)
}
}

View file

@ -36,6 +36,7 @@ fun aSecureBackupEnterRecoveryKeyState(
recoveryKey: String = aFormattedRecoveryKey(),
isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(),
submitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit = {},
) = SecureBackupEnterRecoveryKeyState(
recoveryKeyViewState = RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
@ -44,5 +45,5 @@ fun aSecureBackupEnterRecoveryKeyState(
),
isSubmitEnabled = isSubmitEnabled,
submitAction = submitAction,
eventSink = {}
eventSink = eventSink,
)

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.components.async.AsyncActionVie
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@ -39,6 +40,7 @@ fun SecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState,
onDone: () -> Unit,
onBackClicked: () -> Unit,
onCreateNewRecoveryKey: () -> Unit,
modifier: Modifier = Modifier,
) {
AsyncActionView(
@ -57,7 +59,7 @@ fun SecureBackupEnterRecoveryKeyView(
title = stringResource(id = R.string.screen_recovery_key_confirm_title),
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
content = { Content(state = state) },
buttons = { Buttons(state = state) }
buttons = { Buttons(state = state, onCreateRecoveryKey = onCreateNewRecoveryKey) }
)
}
@ -81,6 +83,7 @@ private fun Content(
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupEnterRecoveryKeyState,
onCreateRecoveryKey: () -> Unit,
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
@ -91,6 +94,12 @@ private fun ColumnScope.Buttons(
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
}
)
TextButton(
text = stringResource(id = R.string.screen_recovery_key_confirm_lost_recovery_key),
enabled = !state.submitAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = onCreateRecoveryKey,
)
}
@PreviewsDayNight
@ -102,5 +111,6 @@ internal fun SecureBackupEnterRecoveryKeyViewPreview(
state = state,
onDone = {},
onBackClicked = {},
onCreateNewRecoveryKey = {},
)
}

View file

@ -35,8 +35,9 @@
<string name="screen_recovery_key_confirm_key_description">"If you have a security key or security phrase, this will work too."</string>
<string name="screen_recovery_key_confirm_key_label">"Recovery key or passcode"</string>
<string name="screen_recovery_key_confirm_key_placeholder">"Enter…"</string>
<string name="screen_recovery_key_confirm_lost_recovery_key">"Lost your recovery key?"</string>
<string name="screen_recovery_key_confirm_success">"Recovery key confirmed"</string>
<string name="screen_recovery_key_confirm_title">"Enter your recovery key or passcode"</string>
<string name="screen_recovery_key_confirm_title">"Enter your recovery key"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Copied recovery key"</string>
<string name="screen_recovery_key_generating_key">"Generating…"</string>
<string name="screen_recovery_key_save_action">"Save recovery key"</string>

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.enter
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SecureBackupEnterRecoveryKeyViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `back key pressed - calls onBackClicked`() {
ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(),
onBackClicked = callback,
)
rule.pressBackKey()
}
}
@Test
fun `back button clicked - calls onBackClicked`() {
ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(),
onBackClicked = callback,
)
rule.pressBack()
}
}
@Test
fun `tapping on Continue when key is valid - calls expected action`() {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
rule.setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder),
)
rule.clickOn(CommonStrings.action_continue)
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit)
}
@Test
fun `tapping on Lost your recovery key - calls onCreateNewRecoveryKey`() {
ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(),
onCreateNewRecoveryKey = callback,
)
rule.clickOn(R.string.screen_recovery_key_confirm_lost_recovery_key)
}
}
@Test
fun `when submit action succeeds - calls onDone`() {
ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)),
onDone = callback,
)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState,
onDone: () -> Unit = EnsureNeverCalled(),
onBackClicked: () -> Unit = EnsureNeverCalled(),
onCreateNewRecoveryKey: () -> Unit = EnsureNeverCalled(),
) {
rule.setContent {
SecureBackupEnterRecoveryKeyView(
state = state,
onDone = onDone,
onBackClicked = onBackClicked,
onCreateNewRecoveryKey = onCreateNewRecoveryKey
)
}
}
}