mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 10:55:55 +03:00
SSO UIA for deactivate account
This commit is contained in:
parent
8129cd0cd3
commit
2a3962265b
13 changed files with 164 additions and 138 deletions
|
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
|
|||
&& 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
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
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.
|
||||
*/
|
||||
|
@ -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
|
||||
* an incomplete view of conversations
|
||||
*/
|
||||
suspend fun deactivateAccount(password: String, eraseAllData: Boolean)
|
||||
suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean)
|
||||
}
|
||||
|
|
|
@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
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)
|
||||
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
|
||||
val erase: Boolean,
|
||||
|
||||
@Json(name = "auth")
|
||||
val auth: Map<String, *>? = null
|
||||
) {
|
||||
companion object {
|
||||
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams {
|
||||
fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams {
|
||||
return DeactivateAccountParams(
|
||||
auth = UserPasswordAuth(user = userId, password = password),
|
||||
auth = auth?.asMap(),
|
||||
erase = erase
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
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.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
|
@ -27,8 +30,9 @@ import javax.inject.Inject
|
|||
|
||||
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
|
||||
data class Params(
|
||||
val password: String,
|
||||
val eraseAllData: Boolean
|
||||
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
|
||||
val eraseAllData: Boolean,
|
||||
val userAuthParam: UIABaseAuth? = null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
|
|||
) : DeactivateAccountTask {
|
||||
|
||||
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) {
|
||||
apiCall = accountAPI.deactivate(deactivateAccountParams)
|
||||
try {
|
||||
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
|
||||
runCatching { identityDisconnectTask.execute(Unit) }
|
||||
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
|
|||
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
|
||||
}
|
||||
|
||||
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) {
|
||||
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData))
|
||||
override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
|
||||
deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
|
|||
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
|
||||
super.onNavigationEvent(navigationEvent, extras)
|
||||
if (navigationEvent == NAVIGATION_FINISHED) {
|
||||
sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
|
||||
// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ class ReAuthViewModel @AssistedInject constructor(
|
|||
when (action) {
|
||||
ReAuthActions.StartSSOFallback -> {
|
||||
if (state.flowType == LoginFlowTypes.SSO) {
|
||||
setState { copy(ssoFallbackPageWasShown = true) }
|
||||
val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "")
|
||||
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.settings.account.deactivation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -23,16 +24,16 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.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.databinding.FragmentDeactivateAccountBinding
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.MainActivityArgs
|
||||
import im.vector.app.features.auth.ReAuthActivity
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor(
|
|||
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() {
|
||||
super.onResume()
|
||||
(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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupUi()
|
||||
setupViewListeners()
|
||||
observeViewEvents()
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
views.deactivateAccountPassword.textChanges()
|
||||
.subscribe {
|
||||
views.deactivateAccountPasswordTil.error = null
|
||||
views.deactivateAccountSubmit.isEnabled = it.isNotEmpty()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun setupViewListeners() {
|
||||
views.deactivateAccountPasswordReveal.setOnClickListener {
|
||||
viewModel.handle(DeactivateAccountAction.TogglePassword)
|
||||
}
|
||||
|
||||
views.deactivateAccountSubmit.debouncedClicks {
|
||||
viewModel.handle(DeactivateAccountAction.DeactivateAccount(
|
||||
views.deactivateAccountPassword.text.toString(),
|
||||
views.deactivateAccountEraseCheckbox.isChecked))
|
||||
views.deactivateAccountEraseCheckbox.isChecked)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewEvents() {
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is DeactivateAccountViewEvents.Loading -> {
|
||||
is DeactivateAccountViewEvents.Loading -> {
|
||||
settingsActivity?.ignoreInvalidTokenError = true
|
||||
showLoadingDialog(it.message)
|
||||
}
|
||||
DeactivateAccountViewEvents.EmptyPassword -> {
|
||||
DeactivateAccountViewEvents.InvalidAuth -> {
|
||||
dismissLoadingDialog()
|
||||
settingsActivity?.ignoreInvalidTokenError = false
|
||||
views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
|
||||
}
|
||||
DeactivateAccountViewEvents.InvalidPassword -> {
|
||||
settingsActivity?.ignoreInvalidTokenError = false
|
||||
views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
|
||||
}
|
||||
is DeactivateAccountViewEvents.OtherFailure -> {
|
||||
is DeactivateAccountViewEvents.OtherFailure -> {
|
||||
settingsActivity?.ignoreInvalidTokenError = false
|
||||
dismissLoadingDialog()
|
||||
displayErrorDialog(it.throwable)
|
||||
}
|
||||
DeactivateAccountViewEvents.Done ->
|
||||
DeactivateAccountViewEvents.Done -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,14 +17,15 @@
|
|||
package im.vector.app.features.settings.account.deactivation
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||
|
||||
/**
|
||||
* 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()
|
||||
object InvalidAuth : DeactivateAccountViewEvents()
|
||||
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
|
||||
object Done : DeactivateAccountViewEvents()
|
||||
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents()
|
||||
}
|
||||
|
|
|
@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState
|
|||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
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 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 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(
|
||||
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) {
|
||||
|
@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
|
|||
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
|
||||
}
|
||||
|
||||
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||
var pendingAuth: UIABaseAuth? = null
|
||||
|
||||
override fun handle(action: DeactivateAccountAction) {
|
||||
when (action) {
|
||||
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
|
||||
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
|
|||
}
|
||||
|
||||
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
|
||||
if (action.password.isEmpty()) {
|
||||
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
|
||||
return
|
||||
}
|
||||
|
||||
_viewEvents.post(DeactivateAccountViewEvents.Loading())
|
||||
|
||||
viewModelScope.launch {
|
||||
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
|
||||
} catch (failure: Exception) {
|
||||
if (failure.isInvalidPassword()) {
|
||||
DeactivateAccountViewEvents.InvalidPassword
|
||||
if (failure.isInvalidUIAAuth()) {
|
||||
DeactivateAccountViewEvents.InvalidAuth
|
||||
} else {
|
||||
DeactivateAccountViewEvents.OtherFailure(failure)
|
||||
}
|
||||
|
|
|
@ -124,16 +124,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
|||
Unit
|
||||
}
|
||||
is CrossSigningSettingsAction.SsoAuthDone -> {
|
||||
// we should use token based auth
|
||||
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||
// will release the interactive auth interceptor
|
||||
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
|
||||
Timber.d("## UIA - FallBack success")
|
||||
if (pendingAuth != null) {
|
||||
uiaContinuation?.resume(pendingAuth!!)
|
||||
} else {
|
||||
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||
}
|
||||
Unit
|
||||
}
|
||||
is CrossSigningSettingsAction.PasswordAuthDone -> {
|
||||
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||
|
|
|
@ -31,75 +31,14 @@
|
|||
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"
|
||||
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
|
||||
android:id="@+id/deactivateAccountSubmit"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/deactivate_account_submit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
Loading…
Reference in a new issue