mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Account deactivation (with password only) (#35)
This commit is contained in:
parent
eca3bf0817
commit
045e3d7bae
@ -7,6 +7,7 @@ Features ✨:
|
|||||||
- Cross-Signing | Verify new session from existing session (#1134)
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||||
- Save media files to Gallery (#973)
|
- Save media files to Gallery (#973)
|
||||||
|
- Account deactivation (with password only) (#35)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||||
|
@ -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.
|
* This interface defines methods to manage the account. It's implemented at the session level.
|
||||||
*/
|
*/
|
||||||
interface AccountService {
|
interface AccountService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the homeserver to change the password.
|
* Ask the homeserver to change the password.
|
||||||
* @param password Current password.
|
* @param password Current password.
|
||||||
* @param newPassword New password
|
* @param newPassword New password
|
||||||
*/
|
*/
|
||||||
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
|
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): 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. <b>This action is irreversible</b>.\n\nDeactivating your account <b>does not by default
|
||||||
|
* cause us to forget messages you have sent</b>. 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<Unit>): Cancelable
|
||||||
}
|
}
|
||||||
|
@ -30,4 +30,12 @@ internal interface AccountAPI {
|
|||||||
*/
|
*/
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
|
||||||
fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
|
fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Unit>
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,9 @@ internal abstract class AccountModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
|
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindAccountService(service: DefaultAccountService): AccountService
|
abstract fun bindAccountService(service: DefaultAccountService): AccountService
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<DeactivateAccountTask.Params, Unit> {
|
||||||
|
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<Unit>(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<Unit>(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
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
|
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
|
||||||
|
private val deactivateAccountTask: DeactivateAccountTask,
|
||||||
private val taskExecutor: TaskExecutor) : AccountService {
|
private val taskExecutor: TaskExecutor) : AccountService {
|
||||||
|
|
||||||
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
|
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
|
|||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return deactivateAccountTask
|
||||||
|
.configureWith(DeactivateAccountTask.Params(password, eraseAllData)) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
|
|||||||
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
||||||
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
|
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
|
||||||
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
|
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.crosssigning.CrossSigningSettingsFragment
|
||||||
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||||
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
||||||
@ -445,8 +446,14 @@ interface FragmentModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
||||||
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(DeactivateAccountFragment::class)
|
||||||
|
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ data class MainActivityArgs(
|
|||||||
val clearCache: Boolean = false,
|
val clearCache: Boolean = false,
|
||||||
val clearCredentials: Boolean = false,
|
val clearCredentials: Boolean = false,
|
||||||
val isUserLoggedOut: Boolean = false,
|
val isUserLoggedOut: Boolean = false,
|
||||||
|
val isAccountDeactivated: Boolean = false,
|
||||||
val isSoftLogout: Boolean = false
|
val isSoftLogout: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() {
|
|||||||
clearCache = argsFromIntent?.clearCache ?: false,
|
clearCache = argsFromIntent?.clearCache ?: false,
|
||||||
clearCredentials = argsFromIntent?.clearCredentials ?: false,
|
clearCredentials = argsFromIntent?.clearCredentials ?: false,
|
||||||
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
|
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
|
||||||
|
isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false,
|
||||||
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
|
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -122,7 +124,7 @@ class MainActivity : VectorBaseActivity() {
|
|||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
args.clearCredentials -> session.signOut(
|
args.clearCredentials -> session.signOut(
|
||||||
!args.isUserLoggedOut,
|
!args.isUserLoggedOut && !args.isAccountDeactivated,
|
||||||
object : MatrixCallback<Unit> {
|
object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.w("SIGN_OUT: success, start app")
|
Timber.w("SIGN_OUT: success, start app")
|
||||||
@ -182,16 +184,16 @@ class MainActivity : VectorBaseActivity() {
|
|||||||
private fun startNextActivityAndFinish() {
|
private fun startNextActivityAndFinish() {
|
||||||
val intent = when {
|
val intent = when {
|
||||||
args.clearCredentials
|
args.clearCredentials
|
||||||
&& !args.isUserLoggedOut ->
|
&& (!args.isUserLoggedOut || args.isAccountDeactivated) ->
|
||||||
// User has explicitly asked to log out
|
// User has explicitly asked to log out or deactivated his account
|
||||||
LoginActivity.newIntent(this, null)
|
LoginActivity.newIntent(this, null)
|
||||||
args.isSoftLogout ->
|
args.isSoftLogout ->
|
||||||
// The homeserver has invalidated the token, with a soft logout
|
// The homeserver has invalidated the token, with a soft logout
|
||||||
SoftLogoutActivity.newIntent(this)
|
SoftLogoutActivity.newIntent(this)
|
||||||
args.isUserLoggedOut ->
|
args.isUserLoggedOut ->
|
||||||
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
|
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
|
||||||
SignedOutActivity.newIntent(this)
|
SignedOutActivity.newIntent(this)
|
||||||
sessionHolder.hasActiveSession() ->
|
sessionHolder.hasActiveSession() ->
|
||||||
// We have a session.
|
// We have a session.
|
||||||
// Check it can be opened
|
// Check it can be opened
|
||||||
if (sessionHolder.getActiveSession().isOpenable) {
|
if (sessionHolder.getActiveSession().isOpenable) {
|
||||||
@ -200,7 +202,7 @@ class MainActivity : VectorBaseActivity() {
|
|||||||
// The token is still invalid
|
// The token is still invalid
|
||||||
SoftLogoutActivity.newIntent(this)
|
SoftLogoutActivity.newIntent(this)
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
// First start, or no active session
|
// First start, or no active session
|
||||||
LoginActivity.newIntent(this, null)
|
LoginActivity.newIntent(this, null)
|
||||||
}
|
}
|
||||||
|
@ -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_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_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"
|
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 SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"
|
||||||
|
|
||||||
private const val MEDIA_SAVING_3_DAYS = 0
|
private const val MEDIA_SAVING_3_DAYS = 0
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Intent
|
|||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import im.vector.matrix.android.api.failure.GlobalError
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
|||||||
|
|
||||||
private var keyToHighlight: String? = null
|
private var keyToHighlight: String? = null
|
||||||
|
|
||||||
|
var ignoreInvalidTokenError = false
|
||||||
|
|
||||||
@Inject lateinit var session: Session
|
@Inject lateinit var session: Session
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
|||||||
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
||||||
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
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)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
||||||
else ->
|
else ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
||||||
@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
|||||||
return keyToHighlight
|
return keyToHighlight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
|
||||||
|
if (ignoreInvalidTokenError) {
|
||||||
|
Timber.w("Ignoring invalid token global error")
|
||||||
|
} else {
|
||||||
|
super.handleInvalidToken(globalError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
|
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
|
||||||
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
|
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
|
||||||
|
@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deactivate account section
|
|
||||||
|
|
||||||
// deactivate account
|
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!!
|
|
||||||
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
activity?.let {
|
|
||||||
notImplemented()
|
|
||||||
// TODO startActivity(DeactivateAccountActivity.getIntent(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(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<Unit> {
|
||||||
|
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<DeactivateAccountViewModel, DeactivateAccountViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? {
|
||||||
|
val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.viewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal file
116
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deactivateAccountContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/deactivate_account_content"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/deactivateAccountEraseCheckbox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="top"
|
||||||
|
android:text="@string/deactivate_account_delete_checkbox"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deactivateAccountPromptPassword"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/deactivate_account_prompt_password"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/deactivateAccountPasswordContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/auth_password_placeholder"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:nextFocusDown="@+id/login_password"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPromptPassword">
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/deactivateAccountPasswordTil"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/login_signup_password_hint"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:errorIconDrawable="@null">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/deactivateAccountPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingEnd="48dp"
|
||||||
|
android:paddingRight="48dp"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/deactivateAccountPasswordReveal"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_eye_black"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
tools:contentDescription="@string/a11y_show_password" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/deactivateAccountSubmit"
|
||||||
|
style="@style/VectorButtonStyleDestructive"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/deactivate_account_submit"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/deactivateAccountCancel"
|
||||||
|
style="@style/VectorButtonStyle"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/deactivateAccountSubmit"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/deactivateAccountSubmit" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
@ -97,14 +97,13 @@
|
|||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_deactivate_account_section">
|
||||||
android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY"
|
|
||||||
android:title="@string/settings_deactivate_account_section"
|
|
||||||
app:isPreferenceVisible="@bool/false_not_implemented">
|
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreference
|
<im.vector.riotx.core.preference.VectorPreference
|
||||||
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"
|
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"
|
||||||
android:title="@string/settings_deactivate_my_account" />
|
android:persistent="false"
|
||||||
|
android:title="@string/settings_deactivate_my_account"
|
||||||
|
app:fragment="im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment" />
|
||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user