SSO UIA for deactivate account

This commit is contained in:
Valere 2021-02-02 12:30:46 +01:00
parent 8129cd0cd3
commit 2a3962265b
13 changed files with 164 additions and 138 deletions

View file

@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
&& error.message == "Invalid password" && error.message == "Invalid password"
} }
fun Throwable.isInvalidUIAAuth(): Boolean {
return this is Failure.ServerError
&& error.code == MatrixError.M_FORBIDDEN
&& error.flows != null
}
/** /**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/ */

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.account package org.matrix.android.sdk.api.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
/** /**
* 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.
*/ */
@ -43,5 +45,5 @@ interface AccountService {
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * @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 * an incomplete view of conversations
*/ */
suspend fun deactivateAccount(password: String, eraseAllData: Boolean) suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean)
} }

View file

@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeactivateAccountParams( internal data class DeactivateAccountParams(
@Json(name = "auth")
val auth: UserPasswordAuth? = null,
// Set to true to erase all data of the account // Set to true to erase all data of the account
@Json(name = "erase") @Json(name = "erase")
val erase: Boolean val erase: Boolean,
@Json(name = "auth")
val auth: Map<String, *>? = null
) { ) {
companion object { companion object {
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams {
return DeactivateAccountParams( return DeactivateAccountParams(
auth = UserPasswordAuth(user = userId, password = password), auth = auth?.asMap(),
erase = erase erase = erase
) )
} }

View file

@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -27,8 +30,9 @@ import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> { internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params( data class Params(
val password: String, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val eraseAllData: Boolean val eraseAllData: Boolean,
val userAuthParam: UIABaseAuth? = null
) )
} }
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
) : DeactivateAccountTask { ) : DeactivateAccountTask {
override suspend fun execute(params: DeactivateAccountTask.Params) { override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
executeRequest<Unit>(globalErrorReceiver) { try {
apiCall = accountAPI.deactivate(deactivateAccountParams) executeRequest<Unit>(globalErrorReceiver) {
apiCall = accountAPI.deactivate(deactivateAccountParams)
}
} catch (throwable: Throwable) {
if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
} }
// Logout from identity server if any, ignoring errors // Logout from identity server if any, ignoring errors
runCatching { identityDisconnectTask.execute(Unit) } runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") } .onFailure { Timber.w(it, "Unable to disconnect identity server") }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.account.AccountService import org.matrix.android.sdk.api.session.account.AccountService
import javax.inject.Inject import javax.inject.Inject
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
} }
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) { override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData)) deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
} }
} }

View file

@ -154,7 +154,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras") Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
super.onNavigationEvent(navigationEvent, extras) super.onNavigationEvent(navigationEvent, extras)
if (navigationEvent == NAVIGATION_FINISHED) { if (navigationEvent == NAVIGATION_FINISHED) {
sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) // sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
} }
} }

View file

@ -54,6 +54,7 @@ class ReAuthViewModel @AssistedInject constructor(
when (action) { when (action) {
ReAuthActions.StartSSOFallback -> { ReAuthActions.StartSSOFallback -> {
if (state.flowType == LoginFlowTypes.SSO) { if (state.flowType == LoginFlowTypes.SSO) {
setState { copy(ssoFallbackPageWasShown = true) }
val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "") val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "")
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL)) _viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
} }

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 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.app.features.settings.account.deactivation
import im.vector.app.core.platform.VectorViewModelAction
sealed class DeactivateAccountAction : VectorViewModelAction {
object TogglePassword : DeactivateAccountAction()
data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction()
object SsoAuthDone: DeactivateAccountAction()
data class PasswordAuthDone(val password: String): DeactivateAccountAction()
object ReAuthCancelled: DeactivateAccountAction()
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.account.deactivation package im.vector.app.features.settings.account.deactivation
import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -23,16 +24,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.showPassword import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.databinding.FragmentDeactivateAccountBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject import javax.inject.Inject
@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor(
return FragmentDeactivateAccountBinding.inflate(inflater, container, false) return FragmentDeactivateAccountBinding.inflate(inflater, container, false)
} }
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(DeactivateAccountAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
}
}
} else {
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
@ -66,59 +86,46 @@ class DeactivateAccountFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupUi()
setupViewListeners() setupViewListeners()
observeViewEvents() observeViewEvents()
} }
private fun setupUi() {
views.deactivateAccountPassword.textChanges()
.subscribe {
views.deactivateAccountPasswordTil.error = null
views.deactivateAccountSubmit.isEnabled = it.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupViewListeners() { private fun setupViewListeners() {
views.deactivateAccountPasswordReveal.setOnClickListener {
viewModel.handle(DeactivateAccountAction.TogglePassword)
}
views.deactivateAccountSubmit.debouncedClicks { views.deactivateAccountSubmit.debouncedClicks {
viewModel.handle(DeactivateAccountAction.DeactivateAccount( viewModel.handle(DeactivateAccountAction.DeactivateAccount(
views.deactivateAccountPassword.text.toString(), views.deactivateAccountEraseCheckbox.isChecked)
views.deactivateAccountEraseCheckbox.isChecked)) )
} }
} }
private fun observeViewEvents() { private fun observeViewEvents() {
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is DeactivateAccountViewEvents.Loading -> { is DeactivateAccountViewEvents.Loading -> {
settingsActivity?.ignoreInvalidTokenError = true settingsActivity?.ignoreInvalidTokenError = true
showLoadingDialog(it.message) showLoadingDialog(it.message)
} }
DeactivateAccountViewEvents.EmptyPassword -> { DeactivateAccountViewEvents.InvalidAuth -> {
dismissLoadingDialog()
settingsActivity?.ignoreInvalidTokenError = false settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
} }
DeactivateAccountViewEvents.InvalidPassword -> { is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
}
is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false settingsActivity?.ignoreInvalidTokenError = false
dismissLoadingDialog()
displayErrorDialog(it.throwable) displayErrorDialog(it.throwable)
} }
DeactivateAccountViewEvents.Done -> DeactivateAccountViewEvents.Done -> {
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
}
is DeactivateAccountViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(),
it.registrationFlowResponse,
it.lastErrorCode,
getString(R.string.deactivate_account_title)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
}.exhaustive }.exhaustive
} }
} }
override fun invalidate() = withState(viewModel) { state ->
views.deactivateAccountPassword.showPassword(state.passwordShown)
views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye)
}
} }

View file

@ -17,14 +17,15 @@
package im.vector.app.features.settings.account.deactivation package im.vector.app.features.settings.account.deactivation
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
/** /**
* Transient events for deactivate account settings screen * Transient events for deactivate account settings screen
*/ */
sealed class DeactivateAccountViewEvents : VectorViewEvents { sealed class DeactivateAccountViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
object EmptyPassword : DeactivateAccountViewEvents() object InvalidAuth : DeactivateAccountViewEvents()
object InvalidPassword : DeactivateAccountViewEvents()
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
object Done : DeactivateAccountViewEvents() object Done : DeactivateAccountViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents()
} }

View file

@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.auth.ReAuthActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.isInvalidUIAAuth
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.lang.Exception import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
data class DeactivateAccountViewState( data class DeactivateAccountViewState(
val passwordShown: Boolean = false val passwordShown: Boolean = false
) : MvRxState ) : 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, class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
private val session: Session) private val session: Session)
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) { : VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
} }
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
override fun handle(action: DeactivateAccountAction) { override fun handle(action: DeactivateAccountAction) {
when (action) { when (action) {
DeactivateAccountAction.TogglePassword -> handleTogglePassword() DeactivateAccountAction.TogglePassword -> handleTogglePassword()
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
DeactivateAccountAction.SsoAuthDone -> {
Timber.d("## UIA - FallBack success")
if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!)
} else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
}
}
is DeactivateAccountAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
}
DeactivateAccountAction.ReAuthCancelled -> {
Timber.d("## UIA - Reauth cancelled")
uiaContinuation?.resumeWith(Result.failure((Exception())))
uiaContinuation = null
pendingAuth = null
}
}.exhaustive }.exhaustive
} }
@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
} }
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
if (action.password.isEmpty()) {
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
return
}
_viewEvents.post(DeactivateAccountViewEvents.Loading()) _viewEvents.post(DeactivateAccountViewEvents.Loading())
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
session.deactivateAccount(action.password, action.eraseAllData) session.deactivateAccount(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
_viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode))
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
uiaContinuation = promise
}
}, action.eraseAllData)
DeactivateAccountViewEvents.Done DeactivateAccountViewEvents.Done
} catch (failure: Exception) { } catch (failure: Exception) {
if (failure.isInvalidPassword()) { if (failure.isInvalidUIAAuth()) {
DeactivateAccountViewEvents.InvalidPassword DeactivateAccountViewEvents.InvalidAuth
} else { } else {
DeactivateAccountViewEvents.OtherFailure(failure) DeactivateAccountViewEvents.OtherFailure(failure)
} }

View file

@ -124,16 +124,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
Unit Unit
} }
is CrossSigningSettingsAction.SsoAuthDone -> { is CrossSigningSettingsAction.SsoAuthDone -> {
// we should use token based auth Timber.d("## UIA - FallBack success")
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
// will release the interactive auth interceptor
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
if (pendingAuth != null) { if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!) uiaContinuation?.resume(pendingAuth!!)
} else { } else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
} }
Unit
} }
is CrossSigningSettingsAction.PasswordAuthDone -> { is CrossSigningSettingsAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)

View file

@ -31,75 +31,14 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" /> 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"
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"
tools:contentDescription="@string/a11y_show_password"
app:tint="?attr/colorAccent"
tools:ignore="MissingPrefix" />
</FrameLayout>
<Button <Button
android:id="@+id/deactivateAccountSubmit" android:id="@+id/deactivateAccountSubmit"
style="@style/VectorButtonStyleDestructive" style="@style/VectorButtonStyleDestructive"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/deactivate_account_submit" android:text="@string/deactivate_account_submit"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" /> app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>