Merge pull request #3360 from element-hq/feature/bma/sessionVerificationBannerIsBack
Add banner entry point to set up recovery
This commit is contained in:
@@ -259,6 +259,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
backstack.push(NavTarget.CreateRoom)
|
||||
}
|
||||
|
||||
override fun onSetUpRecoveryClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
|
||||
}
|
||||
|
||||
override fun onSessionConfirmRecoveryKeyClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||
fun onRoomClick(roomId: RoomId)
|
||||
fun onCreateRoomClick()
|
||||
fun onSettingsClick()
|
||||
fun onSetUpRecoveryClick()
|
||||
fun onSessionConfirmRecoveryKeyClick()
|
||||
fun onRoomSettingsClick(roomId: RoomId)
|
||||
fun onReportBugClick()
|
||||
|
||||
@@ -66,6 +66,10 @@ class RoomListNode @AssistedInject constructor(
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onCreateRoomClick() }
|
||||
}
|
||||
|
||||
private fun onSetUpRecoveryClick() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSetUpRecoveryClick() }
|
||||
}
|
||||
|
||||
private fun onSessionConfirmRecoveryKeyClick() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClick() }
|
||||
}
|
||||
@@ -98,6 +102,7 @@ class RoomListNode @AssistedInject constructor(
|
||||
onRoomClick = this::onRoomClick,
|
||||
onSettingsClick = this::onOpenSettings,
|
||||
onCreateRoomClick = this::onCreateRoomClick,
|
||||
onSetUpRecoveryClick = this::onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
|
||||
onRoomSettingsClick = this::onRoomSettingsClick,
|
||||
onMenuActionClick = { onMenuActionClick(activity, it) },
|
||||
|
||||
@@ -187,8 +187,15 @@ class RoomListPresenter @Inject constructor(
|
||||
derivedStateOf {
|
||||
when {
|
||||
currentSecurityBannerDismissed -> SecurityBannerState.None
|
||||
recoveryState == RecoveryState.INCOMPLETE &&
|
||||
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
||||
syncState == SyncState.Running -> {
|
||||
when (recoveryState) {
|
||||
RecoveryState.UNKNOWN,
|
||||
RecoveryState.DISABLED -> SecurityBannerState.SetUpRecovery
|
||||
RecoveryState.INCOMPLETE -> SecurityBannerState.RecoveryKeyConfirmation
|
||||
RecoveryState.WAITING_FOR_SYNC,
|
||||
RecoveryState.ENABLED -> SecurityBannerState.None
|
||||
}
|
||||
}
|
||||
else -> SecurityBannerState.None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ enum class InvitesState {
|
||||
|
||||
enum class SecurityBannerState {
|
||||
None,
|
||||
SetUpRecovery,
|
||||
RecoveryKeyConfirmation,
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||
aRoomListState(contentState = aSkeletonContentState()),
|
||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ fun RoomListView(
|
||||
state: RoomListState,
|
||||
onRoomClick: (RoomId) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
onRoomSettingsClick: (roomId: RoomId) -> Unit,
|
||||
@@ -78,6 +79,7 @@ fun RoomListView(
|
||||
|
||||
RoomListScaffold(
|
||||
state = state,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
onOpenSettings = onSettingsClick,
|
||||
@@ -106,6 +108,7 @@ fun RoomListView(
|
||||
@Composable
|
||||
private fun RoomListScaffold(
|
||||
state: RoomListState,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomId) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
@@ -142,6 +145,7 @@ private fun RoomListScaffold(
|
||||
contentState = state.contentState,
|
||||
filtersState = state.filtersState,
|
||||
eventSink = state.eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = ::onRoomClick,
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
@@ -178,6 +182,7 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
||||
state = state,
|
||||
onRoomClick = {},
|
||||
onSettingsClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onCreateRoomClick = {},
|
||||
onRoomSettingsClick = {},
|
||||
|
||||
@@ -70,6 +70,7 @@ fun RoomListContentView(
|
||||
contentState: RoomListContentState,
|
||||
filtersState: RoomListFiltersState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
@@ -95,6 +96,7 @@ fun RoomListContentView(
|
||||
state = contentState,
|
||||
filtersState = filtersState,
|
||||
eventSink = eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
)
|
||||
@@ -141,6 +143,7 @@ private fun RoomsView(
|
||||
state: RoomListContentState.Rooms,
|
||||
filtersState: RoomListFiltersState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -154,6 +157,7 @@ private fun RoomsView(
|
||||
RoomsViewList(
|
||||
state = state,
|
||||
eventSink = eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
@@ -165,6 +169,7 @@ private fun RoomsView(
|
||||
private fun RoomsViewList(
|
||||
state: RoomListContentState.Rooms,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -188,21 +193,27 @@ private fun RoomsViewList(
|
||||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
||||
contentPadding = PaddingValues(bottom = 80.dp)
|
||||
) {
|
||||
if (state.securityBannerState != SecurityBannerState.None) {
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.SetUpRecovery -> {
|
||||
item {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onSetUpRecoveryClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) {
|
||||
item {
|
||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SecurityBannerState.None -> if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) {
|
||||
item {
|
||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +287,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||
),
|
||||
eventSink = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onRoomClick = {},
|
||||
onCreateRoomClick = {},
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.roomlist.impl.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
@Composable
|
||||
internal fun SetUpRecoveryKeyBanner(
|
||||
onContinueClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
DialogLikeBannerMolecule(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.banner_set_up_recovery_title),
|
||||
content = stringResource(R.string.banner_set_up_recovery_content),
|
||||
onSubmitClick = onContinueClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SetUpRecoveryKeyBannerPreview() = ElementPreview {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = {},
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
|
||||
<string name="banner_set_up_recovery_title">"Set up recovery"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||
<string name="full_screen_intent_banner_message">"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."</string>
|
||||
|
||||
@@ -264,10 +264,21 @@ class RoomListPresenterTest {
|
||||
val initialState = consumeItemsUntilPredicate {
|
||||
it.contentState is RoomListContentState.Rooms
|
||||
}.last()
|
||||
assertThat(initialState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
assertThat(initialState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
val nextState = awaitItem()
|
||||
assertThat(nextState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||
// Also check other states
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
||||
encryptionService.emitRecoveryState(RecoveryState.WAITING_FOR_SYNC)
|
||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
||||
encryptionService.emitRecoveryState(RecoveryState.ENABLED)
|
||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
||||
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
|
||||
@@ -80,6 +80,24 @@ class RoomListViewTest {
|
||||
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on close setup key banner emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
val close = rule.activity.getString(CommonStrings.action_close)
|
||||
rule.onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on continue recovery key banner invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
@@ -101,6 +119,27 @@ class RoomListViewTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on continue setup key banner invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSetUpRecoveryClick = callback,
|
||||
)
|
||||
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
|
||||
eventsRecorder.assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on start chat when the session has no room invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
||||
@@ -208,6 +247,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||
state: RoomListState,
|
||||
onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSetUpRecoveryClick: () -> Unit = EnsureNeverCalled(),
|
||||
onConfirmRecoveryKeyClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
@@ -219,6 +259,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||
state = state,
|
||||
onRoomClick = onRoomClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
onRoomSettingsClick = onRoomSettingsClick,
|
||||
|
||||
@@ -29,6 +29,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
|
||||
@Parcelize
|
||||
data object Root : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object SetUpRecovery : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : InitialTarget
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
backstack = BackStack(
|
||||
initialElement = when (plugins.filterIsInstance<SecureBackupEntryPoint.Params>().first().initialElement) {
|
||||
SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root
|
||||
SecureBackupEntryPoint.InitialTarget.SetUpRecovery -> NavTarget.Setup
|
||||
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
|
||||
is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity
|
||||
},
|
||||
|
||||
@@ -178,6 +178,7 @@ class RoomListScreen(
|
||||
state = state,
|
||||
onRoomClick = ::onRoomClick,
|
||||
onSettingsClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onCreateRoomClick = {},
|
||||
onRoomSettingsClick = {},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -138,6 +138,7 @@
|
||||
"screen_roomlist_.*",
|
||||
"session_verification_banner_.*",
|
||||
"confirm_recovery_key_banner_.*",
|
||||
"banner\\.set_up_recovery\\..*",
|
||||
"full_screen_intent_banner_.*",
|
||||
"screen_migration_.*",
|
||||
"screen_invites_.*"
|
||||
|
||||
Reference in New Issue
Block a user