Skip to content

Commit f0bb1fa

Browse files
authored
Merge pull request #3479 from element-hq/bma/accountDeactivation
Account deactivation.
2 parents b94a5c9 + 782e1e3 commit f0bb1fa

File tree

43 files changed

+1137
-22
lines changed

Some content is hidden

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

43 files changed

+1137
-22
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
plugins {
8+
id("io.element.android-compose-library")
9+
}
10+
11+
android {
12+
namespace = "io.element.android.features.deactivation.api"
13+
}
14+
15+
dependencies {
16+
implementation(projects.libraries.architecture)
17+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.deactivation.api
9+
10+
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
11+
12+
interface AccountDeactivationEntryPoint : SimpleFeatureEntryPoint
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
plugins {
9+
id("io.element.android-compose-library")
10+
alias(libs.plugins.anvil)
11+
id("kotlin-parcelize")
12+
}
13+
14+
android {
15+
namespace = "io.element.android.features.deactivation.impl"
16+
17+
testOptions {
18+
unitTests {
19+
isIncludeAndroidResources = true
20+
}
21+
}
22+
}
23+
24+
anvil {
25+
generateDaggerFactories.set(true)
26+
}
27+
28+
dependencies {
29+
implementation(projects.anvilannotations)
30+
anvil(projects.anvilcodegen)
31+
implementation(projects.libraries.androidutils)
32+
implementation(projects.libraries.core)
33+
implementation(projects.libraries.architecture)
34+
implementation(projects.libraries.matrix.api)
35+
implementation(projects.libraries.designsystem)
36+
implementation(projects.libraries.uiStrings)
37+
api(projects.features.deactivation.api)
38+
39+
testImplementation(libs.test.junit)
40+
testImplementation(libs.coroutines.test)
41+
testImplementation(libs.molecule.runtime)
42+
testImplementation(libs.test.truth)
43+
testImplementation(libs.test.turbine)
44+
testImplementation(libs.test.robolectric)
45+
testImplementation(libs.androidx.compose.ui.test.junit)
46+
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
47+
testImplementation(projects.libraries.matrix.test)
48+
testImplementation(projects.tests.testutils)
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.logout.impl
9+
10+
sealed interface AccountDeactivationEvents {
11+
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents
12+
data class SetPassword(val password: String) : AccountDeactivationEvents
13+
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents
14+
data object CloseDialogs : AccountDeactivationEvents
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.logout.impl
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import com.bumble.appyx.core.modality.BuildContext
13+
import com.bumble.appyx.core.node.Node
14+
import com.bumble.appyx.core.plugin.Plugin
15+
import dagger.assisted.Assisted
16+
import dagger.assisted.AssistedInject
17+
import io.element.android.anvilannotations.ContributesNode
18+
import io.element.android.libraries.di.SessionScope
19+
20+
@ContributesNode(SessionScope::class)
21+
class AccountDeactivationNode @AssistedInject constructor(
22+
@Assisted buildContext: BuildContext,
23+
@Assisted plugins: List<Plugin>,
24+
private val presenter: AccountDeactivationPresenter,
25+
) : Node(buildContext, plugins = plugins) {
26+
@Composable
27+
override fun View(modifier: Modifier) {
28+
val state = presenter.present()
29+
AccountDeactivationView(
30+
state = state,
31+
onBackClick = ::navigateUp,
32+
modifier = modifier,
33+
)
34+
}
35+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.logout.impl
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.MutableState
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.rememberCoroutineScope
15+
import io.element.android.libraries.architecture.AsyncAction
16+
import io.element.android.libraries.architecture.Presenter
17+
import io.element.android.libraries.architecture.runCatchingUpdatingState
18+
import io.element.android.libraries.matrix.api.MatrixClient
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.launch
21+
import javax.inject.Inject
22+
23+
class AccountDeactivationPresenter @Inject constructor(
24+
private val matrixClient: MatrixClient,
25+
) : Presenter<AccountDeactivationState> {
26+
@Composable
27+
override fun present(): AccountDeactivationState {
28+
val localCoroutineScope = rememberCoroutineScope()
29+
val action: MutableState<AsyncAction<Unit>> = remember {
30+
mutableStateOf(AsyncAction.Uninitialized)
31+
}
32+
33+
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
34+
35+
fun handleEvents(event: AccountDeactivationEvents) {
36+
when (event) {
37+
is AccountDeactivationEvents.SetEraseData -> {
38+
updateFormState(formState) {
39+
copy(eraseData = event.eraseData)
40+
}
41+
}
42+
is AccountDeactivationEvents.SetPassword -> {
43+
updateFormState(formState) {
44+
copy(password = event.password)
45+
}
46+
}
47+
is AccountDeactivationEvents.DeactivateAccount ->
48+
if (action.value.isConfirming() || event.isRetry) {
49+
localCoroutineScope.deactivateAccount(
50+
formState = formState.value,
51+
action
52+
)
53+
} else {
54+
action.value = AsyncAction.Confirming
55+
}
56+
AccountDeactivationEvents.CloseDialogs -> {
57+
action.value = AsyncAction.Uninitialized
58+
}
59+
}
60+
}
61+
62+
return AccountDeactivationState(
63+
deactivateFormState = formState.value,
64+
accountDeactivationAction = action.value,
65+
eventSink = ::handleEvents
66+
)
67+
}
68+
69+
private fun updateFormState(formState: MutableState<DeactivateFormState>, updateLambda: DeactivateFormState.() -> DeactivateFormState) {
70+
formState.value = updateLambda(formState.value)
71+
}
72+
73+
private fun CoroutineScope.deactivateAccount(
74+
formState: DeactivateFormState,
75+
action: MutableState<AsyncAction<Unit>>,
76+
) = launch {
77+
suspend {
78+
matrixClient.deactivateAccount(
79+
password = formState.password,
80+
eraseData = formState.eraseData,
81+
).getOrThrow()
82+
}.runCatchingUpdatingState(action)
83+
}
84+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.logout.impl
9+
10+
import android.os.Parcelable
11+
import io.element.android.libraries.architecture.AsyncAction
12+
import kotlinx.parcelize.Parcelize
13+
14+
data class AccountDeactivationState(
15+
val deactivateFormState: DeactivateFormState,
16+
val accountDeactivationAction: AsyncAction<Unit>,
17+
val eventSink: (AccountDeactivationEvents) -> Unit,
18+
) {
19+
val submitEnabled: Boolean
20+
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
21+
deactivateFormState.password.isNotEmpty()
22+
}
23+
24+
@Parcelize
25+
data class DeactivateFormState(
26+
val eraseData: Boolean,
27+
val password: String
28+
) : Parcelable {
29+
companion object {
30+
val Default = DeactivateFormState(false, "")
31+
}
32+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.logout.impl
9+
10+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.libraries.architecture.AsyncAction
12+
13+
open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDeactivationState> {
14+
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
15+
override val values: Sequence<AccountDeactivationState>
16+
get() = sequenceOf(
17+
anAccountDeactivationState(),
18+
anAccountDeactivationState(
19+
deactivateFormState = filledForm
20+
),
21+
anAccountDeactivationState(
22+
deactivateFormState = filledForm,
23+
accountDeactivationAction = AsyncAction.Confirming,
24+
),
25+
anAccountDeactivationState(
26+
deactivateFormState = filledForm,
27+
accountDeactivationAction = AsyncAction.Loading
28+
),
29+
anAccountDeactivationState(
30+
deactivateFormState = filledForm,
31+
accountDeactivationAction = AsyncAction.Failure(Exception("Failed to deactivate account"))
32+
),
33+
)
34+
}
35+
36+
internal fun aDeactivateFormState(
37+
eraseData: Boolean = false,
38+
password: String = "",
39+
) = DeactivateFormState(
40+
eraseData = eraseData,
41+
password = password,
42+
)
43+
44+
internal fun anAccountDeactivationState(
45+
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
46+
accountDeactivationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
47+
eventSink: (AccountDeactivationEvents) -> Unit = {},
48+
) = AccountDeactivationState(
49+
deactivateFormState = deactivateFormState,
50+
accountDeactivationAction = accountDeactivationAction,
51+
eventSink = eventSink,
52+
)

0 commit comments

Comments
 (0)