From 045e3d7bae1f6b6ce75791a0efe8d6dcc0366c95 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 21 Apr 2020 20:31:54 +0200 Subject: [PATCH] Account deactivation (with password only) (#35) --- CHANGES.md | 1 + .../api/session/account/AccountService.kt | 19 ++- .../internal/session/account/AccountAPI.kt | 8 ++ .../internal/session/account/AccountModule.kt | 3 + .../account/DeactivateAccountParams.kt | 40 ++++++ .../session/account/DeactivateAccountTask.kt | 77 +++++++++++ .../session/account/DefaultAccountService.kt | 9 ++ .../im/vector/riotx/core/di/FragmentModule.kt | 7 + .../im/vector/riotx/features/MainActivity.kt | 16 ++- .../features/settings/VectorPreferences.kt | 1 - .../settings/VectorSettingsActivity.kt | 13 +- .../settings/VectorSettingsGeneralFragment.kt | 13 -- .../deactivation/DeactivateAccountFragment.kt | 124 ++++++++++++++++++ .../DeactivateAccountViewEvents.kt | 30 +++++ .../DeactivateAccountViewModel.kt | 93 +++++++++++++ .../layout/fragment_deactivate_account.xml | 116 ++++++++++++++++ .../main/res/xml/vector_settings_general.xml | 9 +- 17 files changed, 551 insertions(+), 28 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_deactivate_account.xml diff --git a/CHANGES.md b/CHANGES.md index 0c1d209f61..f43294ca9f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: - Cross-Signing | Verify new session from existing session (#1134) - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) - Save media files to Gallery (#973) + - Account deactivation (with password only) (#35) Improvements 🙌: - Verification DM / Handle concurrent .start after .ready (#794) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt index 68643ff723..ddbaaea6ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt @@ -23,11 +23,28 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to manage the account. It's implemented at the session level. */ interface AccountService { - /** * Ask the homeserver to change the password. * @param password Current password. * @param newPassword New password */ fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable + + /** + * Deactivate the account. + * + * This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register + * the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account + * details from your identity server. This action is irreversible.\n\nDeactivating your account does not by default + * cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below. + * + * Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not + * be shared with any new or unregistered users, but registered users who already have access to these messages will still + * have access to their copy. + * + * @param password the account password + * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see + * an incomplete view of conversations + */ + fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt index 23d8210e89..e7fbd3748c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt @@ -30,4 +30,12 @@ internal interface AccountAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") fun changePassword(@Body params: ChangePasswordParams): Call + + /** + * Deactivate the user account + * + * @param params the deactivate account params + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate") + fun deactivate(@Body params: DeactivateAccountParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt index 87e003b0d3..032139ce5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt @@ -39,6 +39,9 @@ internal abstract class AccountModule { @Binds abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask + @Binds + abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask + @Binds abstract fun bindAccountService(service: DefaultAccountService): AccountService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt new file mode 100644 index 0000000000..7a099ca03d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 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 im.vector.matrix.android.internal.session.account; + +import com.squareup.moshi.Json; +import com.squareup.moshi.JsonClass; +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth + +@JsonClass(generateAdapter = true) +internal data class DeactivateAccountParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + // Set to true to erase all data of the account + @Json(name = "erase") + val erase: Boolean +) { + companion object { + fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + return DeactivateAccountParams( + auth = UserPasswordAuth(user = userId, password = password), + erase = erase + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt new file mode 100644 index 0000000000..b8b57a24a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 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 im.vector.matrix.android.internal.session.account + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeactivateAccountTask : Task { + data class Params( + val password: String, + val eraseAllData: Boolean + ) +} + +internal class DefaultDeactivateAccountTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String +) : DeactivateAccountTask { + + override suspend fun execute(params: DeactivateAccountTask.Params) { + val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + try { + executeRequest(eventBus) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError + && throwable.httpCode == 401 + /* Avoid infinite loop */ + && deactivateAccountParams.auth?.session == null) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(throwable.errorBody) + } catch (e: Exception) { + null + }?.let { + // Retry with authentication + try { + executeRequest(eventBus) { + apiCall = accountAPI.deactivate( + deactivateAccountParams.copy(auth = deactivateAccountParams.auth?.copy(session = it.session)) + ) + } + return + } catch (failure: Throwable) { + throw failure + } + } + } + throw throwable + } + + // TODO This task should also do the cleanup + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt index fce01994d3..f6db1dd3db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask, + private val deactivateAccountTask: DeactivateAccountTask, private val taskExecutor: TaskExecutor) : AccountService { override fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable { @@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw } .executeBy(taskExecutor) } + + override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable { + return deactivateAccountTask + .configureWith(DeactivateAccountTask.Params(password, eraseAllData)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c2f2959bd7..d22d80c4b3 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment +import im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment @@ -445,8 +446,14 @@ interface FragmentModule { @IntoMap @FragmentKey(BootstrapAccountPasswordFragment::class) fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment + @Binds @IntoMap @FragmentKey(BootstrapMigrateBackupFragment::class) fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(DeactivateAccountFragment::class) + fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index bc5a1aff95..9c5569f66d 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -49,6 +49,7 @@ data class MainActivityArgs( val clearCache: Boolean = false, val clearCredentials: Boolean = false, val isUserLoggedOut: Boolean = false, + val isAccountDeactivated: Boolean = false, val isSoftLogout: Boolean = false ) : Parcelable @@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() { clearCache = argsFromIntent?.clearCache ?: false, clearCredentials = argsFromIntent?.clearCredentials ?: false, isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false, + isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false, isSoftLogout = argsFromIntent?.isSoftLogout ?: false ) } @@ -122,7 +124,7 @@ class MainActivity : VectorBaseActivity() { } when { args.clearCredentials -> session.signOut( - !args.isUserLoggedOut, + !args.isUserLoggedOut && !args.isAccountDeactivated, object : MatrixCallback { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: success, start app") @@ -182,16 +184,16 @@ class MainActivity : VectorBaseActivity() { private fun startNextActivityAndFinish() { val intent = when { args.clearCredentials - && !args.isUserLoggedOut -> - // User has explicitly asked to log out + && (!args.isUserLoggedOut || args.isAccountDeactivated) -> + // User has explicitly asked to log out or deactivated his account LoginActivity.newIntent(this, null) - args.isSoftLogout -> + args.isSoftLogout -> // The homeserver has invalidated the token, with a soft logout SoftLogoutActivity.newIntent(this) - args.isUserLoggedOut -> + args.isUserLoggedOut -> // the homeserver has invalidated the token (password changed, device deleted, other security reasons) SignedOutActivity.newIntent(this) - sessionHolder.hasActiveSession() -> + sessionHolder.hasActiveSession() -> // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { @@ -200,7 +202,7 @@ class MainActivity : VectorBaseActivity() { // The token is still invalid SoftLogoutActivity.newIntent(this) } - else -> + else -> // First start, or no active session LoginActivity.newIntent(this, null) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index f0a5a8ace8..e765f961dd 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -159,7 +159,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY" private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK" private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY" - const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY" private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" private const val MEDIA_SAVING_3_DAYS = 0 diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 5db14fdbd2..6d00f02c97 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -20,6 +20,7 @@ import android.content.Intent import androidx.fragment.app.FragmentManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(), private var keyToHighlight: String? = null + var ignoreInvalidTokenError = false + @Inject lateinit var session: Session override fun injectWith(injector: ScreenComponent) { @@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(), when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) @@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(), return keyToHighlight } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + if (ignoreInvalidTokenError) { + Timber.w("Ignoring invalid token global error") + } else { + super.handleInvalidToken(globalError) + } + } + companion object { fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index f754064fbc..802cf7b33f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { false } - - // Deactivate account section - - // deactivate account - findPreference(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.let { - notImplemented() - // TODO startActivity(DeactivateAccountActivity.getIntent(it)) - } - - false - } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt new file mode 100644 index 0000000000..487d913b7e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020 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 im.vector.riotx.features.settings.account.deactivation + +import android.content.Context +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import im.vector.riotx.features.settings.VectorSettingsActivity +import kotlinx.android.synthetic.main.fragment_deactivate_account.* +import javax.inject.Inject + +class DeactivateAccountFragment @Inject constructor( + val viewModelFactory: DeactivateAccountViewModel.Factory +) : VectorBaseFragment() { + + private val viewModel: DeactivateAccountViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_deactivate_account + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) + } + + private var settingsActivity: VectorSettingsActivity? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + settingsActivity = context as? VectorSettingsActivity + } + + override fun onDetach() { + super.onDetach() + settingsActivity = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupViewListeners() + observeViewEvents() + } + + private fun setupUi() { + deactivateAccountPassword.textChanges() + .subscribe { + deactivateAccountPasswordTil.error = null + deactivateAccountSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + } + + private fun setupViewListeners() { + deactivateAccountPasswordReveal.setOnClickListener { + viewModel.handle(DeactivateAccountAction.TogglePassword) + } + + deactivateAccountCancel.setOnClickListener { + (activity as? VectorBaseActivity)?.onBackPressed() + } + + deactivateAccountSubmit.setOnClickListener { + viewModel.handle(DeactivateAccountAction.DeactivateAccount( + deactivateAccountPassword.text.toString(), + deactivateAccountEraseCheckbox.isChecked)) + } + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is DeactivateAccountViewEvents.Loading -> { + settingsActivity?.ignoreInvalidTokenError = true + showLoadingDialog(it.message) + } + DeactivateAccountViewEvents.EmptyPassword -> { + settingsActivity?.ignoreInvalidTokenError = false + deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) + } + DeactivateAccountViewEvents.InvalidPassword -> { + settingsActivity?.ignoreInvalidTokenError = false + deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password) + } + is DeactivateAccountViewEvents.OtherFailure -> { + settingsActivity?.ignoreInvalidTokenError = false + displayErrorDialog(it.throwable) + } + DeactivateAccountViewEvents.Done -> + MainActivity.restartApp(activity!!, MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + + }.exhaustive + } + } + + override fun invalidate() = withState(viewModel) { state -> + deactivateAccountPassword.showPassword(state.passwordShown) + deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt new file mode 100644 index 0000000000..4e7f7252e2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 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 im.vector.riotx.features.settings.account.deactivation + +import im.vector.riotx.core.platform.VectorViewEvents + +/** + * Transient events for deactivate account settings screen + */ +sealed class DeactivateAccountViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() + object EmptyPassword : DeactivateAccountViewEvents() + object InvalidPassword : DeactivateAccountViewEvents() + data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() + object Done : DeactivateAccountViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt new file mode 100644 index 0000000000..adfc9ff5ae --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 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 im.vector.riotx.features.settings.account.deactivation + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction + +data class DeactivateAccountViewState( + val passwordShown: Boolean = false +) : MvRxState + +sealed class DeactivateAccountAction : VectorViewModelAction { + object TogglePassword : DeactivateAccountAction() + data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction() +} + +class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel + } + + override fun handle(action: DeactivateAccountAction) { + when (action) { + DeactivateAccountAction.TogglePassword -> handleTogglePassword() + is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) + }.exhaustive + } + + private fun handleTogglePassword() = withState { + setState { + copy(passwordShown = !passwordShown) + } + } + + private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { + if (action.password.isEmpty()) { + _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword) + return + } + + _viewEvents.post(DeactivateAccountViewEvents.Loading()) + + session.deactivateAccount(action.password, action.eraseAllData, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _viewEvents.post(DeactivateAccountViewEvents.Done) + } + + override fun onFailure(failure: Throwable) { + if (failure.isInvalidPassword()) { + _viewEvents.post(DeactivateAccountViewEvents.InvalidPassword) + } else { + _viewEvents.post(DeactivateAccountViewEvents.OtherFailure(failure)) + } + } + }) + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? { + val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml new file mode 100644 index 0000000000..aca8adf12d --- /dev/null +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + +