Account deactivation.

This commit is contained in:
Benoit Marty 2024-09-17 13:19:46 +02:00
parent b94a5c9c51
commit b87bec6228
29 changed files with 1071 additions and 9 deletions

View file

@ -20,6 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint
@ -52,6 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor(
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
private val logoutEntryPoint: LogoutEntryPoint,
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -100,6 +102,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data object SignOut : NavTarget
@Parcelize
data object AccountDeactivation : NavTarget
@Parcelize
data object OssLicenses : NavTarget
}
@ -151,6 +156,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onSignOutClick() {
backstack.push(NavTarget.SignOut)
}
override fun onOpenAccountDeactivation() {
backstack.push(NavTarget.AccountDeactivation)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@ -236,6 +245,9 @@ class PreferencesFlowNode @AssistedInject constructor(
is NavTarget.OssLicenses -> {
openSourceLicensesEntryPoint.getNode(this, buildContext)
}
NavTarget.AccountDeactivation -> {
accountDeactivationEntryPoint.createNode(this, buildContext)
}
}
}

View file

@ -45,6 +45,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenUserProfile(matrixUser: MatrixUser)
fun onOpenBlockedUsers()
fun onSignOutClick()
fun onOpenAccountDeactivation()
}
private fun onOpenBugReport() {
@ -105,6 +106,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onSignOutClick() }
}
private fun onOpenAccountDeactivation() {
plugins<Callback>().forEach { it.onOpenAccountDeactivation() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -132,6 +137,7 @@ class PreferencesRootNode @AssistedInject constructor(
onSignOutClick()
}
},
onDeactivateClick = this::onOpenAccountDeactivation
)
directLogoutView.Render(

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.architecture.Presenter
@ -75,6 +76,12 @@ class PreferencesRootPresenter @Inject constructor(
val devicesManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
var canDeactivateAccount by remember {
mutableStateOf(false)
}
LaunchedEffect(Unit) {
canDeactivateAccount = matrixClient.canDeactivateAccount()
}
val showBlockedUsersItem by produceState(initialValue = false) {
matrixClient.ignoredUsersFlow
@ -108,6 +115,7 @@ class PreferencesRootPresenter @Inject constructor(
devicesManagementUrl = devicesManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings,
canDeactivateAccount = canDeactivateAccount,
showNotificationSettings = showNotificationSettings.value,
showLockScreenSettings = showLockScreenSettings.value,
showBlockedUsersItem = showBlockedUsersItem,

View file

@ -22,6 +22,7 @@ data class PreferencesRootState(
val devicesManagementUrl: String?,
val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean,
val canDeactivateAccount: Boolean,
val showLockScreenSettings: Boolean,
val showNotificationSettings: Boolean,
val showBlockedUsersItem: Boolean,

View file

@ -29,6 +29,7 @@ fun aPreferencesRootState(
showNotificationSettings = true,
showLockScreenSettings = true,
showBlockedUsersItem = true,
canDeactivateAccount = true,
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
directLogoutState = aDirectLogoutState(),
eventSink = eventSink,

View file

@ -57,6 +57,7 @@ fun PreferencesRootView(
onOpenUserProfile: (MatrixUser) -> Unit,
onOpenBlockedUsers: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@ -99,6 +100,7 @@ fun PreferencesRootView(
onOpenAdvancedSettings = onOpenAdvancedSettings,
onOpenDeveloperSettings = onOpenDeveloperSettings,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
Footer(
@ -193,6 +195,7 @@ private fun ColumnScope.GeneralSection(
onOpenAdvancedSettings: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
@ -225,6 +228,14 @@ private fun ColumnScope.GeneralSection(
style = ListItemStyle.Destructive,
onClick = onSignOutClick,
)
if (state.canDeactivateAccount) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())),
style = ListItemStyle.Destructive,
onClick = onDeactivateClick,
)
}
}
@Composable
@ -292,5 +303,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onOpenUserProfile = {},
onOpenBlockedUsers = {},
onSignOutClick = {},
onDeactivateClick = {},
)
}

View file

@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.root
import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
@ -45,7 +46,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val matrixClient = FakeMatrixClient()
val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true })
val presenter = createPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -76,11 +77,27 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.showDeveloperSettings).isTrue()
assertThat(loadedState.showLockScreenSettings).isTrue()
assertThat(loadedState.showNotificationSettings).isTrue()
assertThat(loadedState.canDeactivateAccount).isTrue()
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState)
assertThat(loadedState.snackbarMessage).isNull()
}
}
@Test
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
val presenter = createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { false }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val loadedState = awaitFirstItem()
assertThat(loadedState.canDeactivateAccount).isFalse()
}
}
@Test
fun `present - developer settings is hidden by default in release builds`() = runTest {
val presenter = createPresenter(
@ -89,8 +106,7 @@ class PreferencesRootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem()
val loadedState = awaitFirstItem()
assertThat(loadedState.showDeveloperSettings).isFalse()
}
}
@ -103,20 +119,22 @@ class PreferencesRootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem()
val loadedState = awaitFirstItem()
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
assertThat(loadedState.showDeveloperSettings).isFalse()
loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick)
}
assertThat(awaitItem().showDeveloperSettings).isTrue()
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(),
matrixClient: FakeMatrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
) = PreferencesRootPresenter(