Soft Logout - request homeserver login flow

This commit is contained in:
Benoit Marty 2019-12-12 20:24:46 +01:00
parent a464c910f8
commit 6811d31a6d
7 changed files with 226 additions and 79 deletions

View File

@ -41,6 +41,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout) stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException -> throwable.ioException is UnknownHostException ->
// Invalid homeserver? // Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)
else -> else ->
stringProvider.getString(R.string.error_no_network) stringProvider.getString(R.string.error_no_network)

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.signout
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class SoftLogoutAction : VectorViewModelAction { sealed class SoftLogoutAction : VectorViewModelAction {
object RetryLoginFlow : SoftLogoutAction()
data class SignInAgain(val password: String) : SoftLogoutAction() data class SignInAgain(val password: String) : SoftLogoutAction()
// TODO Add reset pwd... // TODO Add reset pwd...
} }

View File

@ -30,18 +30,22 @@ import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.login.LoginMode
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_soft_logout.* import kotlinx.android.synthetic.main.fragment_soft_logout.*
import kotlinx.android.synthetic.main.item_error_retry.*
import javax.inject.Inject import javax.inject.Inject
/** /**
* In this screen: * In this screen:
* - the user is asked to enter a password to sign in again to a homeserver. * - the user is asked to enter a password to sign in again to a homeserver.
* - or to cleanup all the data * - or to cleanup all the data
* TODO: migrate to Epoxy (along with all the login screen?)
*/ */
class SoftLogoutFragment @Inject constructor( class SoftLogoutFragment @Inject constructor(
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter
@ -67,6 +71,11 @@ class SoftLogoutFragment @Inject constructor(
} }
} }
@OnClick(R.id.itemErrorRetryButton)
fun retry() {
softLogoutViewModel.handle(SoftLogoutAction.RetryLoginFlow)
}
@OnClick(R.id.softLogoutSubmit) @OnClick(R.id.softLogoutSubmit)
fun submit() { fun submit() {
cleanupUi() cleanupUi()
@ -75,29 +84,36 @@ class SoftLogoutFragment @Inject constructor(
softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password)) softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password))
} }
@OnClick(R.id.softLogoutFormSsoSubmit)
fun ssoSubmit() {
// TODO
}
@OnClick(R.id.softLogoutClearDataSubmit) @OnClick(R.id.softLogoutClearDataSubmit)
fun clearData() = withState(softLogoutViewModel) { state -> fun clearData() {
cleanupUi() withState(softLogoutViewModel) { state ->
cleanupUi()
val messageResId = if (state.hasUnsavedKeys) { val messageResId = if (state.hasUnsavedKeys) {
R.string.soft_logout_clear_data_dialog_content R.string.soft_logout_clear_data_dialog_content
} else { } else {
R.string.soft_logout_clear_data_dialog_e2e_warning_content R.string.soft_logout_clear_data_dialog_e2e_warning_content
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.soft_logout_clear_data_dialog_title)
.setMessage(messageResId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(
clearCache = true,
clearCredentials = true,
isUserLoggedOut = true
))
}
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.soft_logout_clear_data_dialog_title)
.setMessage(messageResId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(
clearCache = true,
clearCredentials = true,
isUserLoggedOut = true
))
}
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
private fun cleanupUi() { private fun cleanupUi() {
@ -114,6 +130,14 @@ class SoftLogoutFragment @Inject constructor(
softLogoutE2eWarningNotice.isVisible = state.hasUnsavedKeys softLogoutE2eWarningNotice.isVisible = state.hasUnsavedKeys
} }
private fun setupForm(state: SoftLogoutViewState) {
softLogoutFormLoading.isVisible = state.asyncHomeServerLoginFlowRequest is Loading
softLogoutFormSsoSubmit.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso
softLogoutFormPassword.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Password
softLogoutFormError.isVisible = state.asyncHomeServerLoginFlowRequest is Fail
itemErrorRetryText.setTextOrHide((state.asyncHomeServerLoginFlowRequest as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) })
}
private fun setupSubmitButton() { private fun setupSubmitButton() {
softLogoutPasswordField.textChanges() softLogoutPasswordField.textChanges()
.map { it.trim().isNotEmpty() } .map { it.trim().isNotEmpty() }
@ -156,6 +180,7 @@ class SoftLogoutFragment @Inject constructor(
override fun invalidate() = withState(softLogoutViewModel) { state -> override fun invalidate() = withState(softLogoutViewModel) { state ->
setupUi(state) setupUi(state)
setupForm(state)
setupAutoFill() setupAutoFill()
when (state.asyncLoginAction) { when (state.asyncLoginAction) {

View File

@ -20,12 +20,16 @@ import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.extensions.toReducedUrl import im.vector.riotx.core.extensions.toReducedUrl
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.login.LoginMode
/** /**
* *
@ -33,8 +37,9 @@ import im.vector.riotx.core.platform.VectorViewModel
class SoftLogoutViewModel @AssistedInject constructor( class SoftLogoutViewModel @AssistedInject constructor(
@Assisted initialState: SoftLogoutViewState, @Assisted initialState: SoftLogoutViewState,
private val session: Session, private val session: Session,
private val activeSessionHolder: ActiveSessionHolder) private val activeSessionHolder: ActiveSessionHolder,
: VectorViewModel<SoftLogoutViewState, SoftLogoutAction>(initialState) { private val authenticationService: AuthenticationService
) : VectorViewModel<SoftLogoutViewState, SoftLogoutAction>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -63,13 +68,83 @@ class SoftLogoutViewModel @AssistedInject constructor(
private var currentTask: Cancelable? = null private var currentTask: Cancelable? = null
init {
// Get the supported login flow
getSupportedLoginFlow()
}
private fun getSupportedLoginFlow() {
val homeServerConnectionConfig = session.sessionParams.homeServerConnectionConfig
currentTask?.cancel()
currentTask = null
authenticationService.cancelPendingLoginOrRegistration()
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
// TODO _viewEvents.post(LoginViewEvents.Error(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(failure)
)
}
}
override fun onSuccess(data: LoginFlowResult) {
when (data) {
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
}
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
|| loginMode == LoginMode.Unsupported) {
notSupported()
} else {
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
}
}
is LoginFlowResult.OutdatedHomeserver -> {
notSupported()
}
}
}
private fun notSupported() {
// Should not happen since it's a re-logout
// Notify the UI
// _viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(IllegalStateException("Should not happen"))
)
}
}
})
}
// TODO Cleanup // TODO Cleanup
// private val _viewEvents = PublishDataSource<LoginViewEvents>() // private val _viewEvents = PublishDataSource<LoginViewEvents>()
// val viewEvents: DataSource<LoginViewEvents> = _viewEvents // val viewEvents: DataSource<LoginViewEvents> = _viewEvents
override fun handle(action: SoftLogoutAction) { override fun handle(action: SoftLogoutAction) {
when (action) { when (action) {
is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow()
is SoftLogoutAction.SignInAgain -> handleSignInAgain(action)
} }
} }

View File

@ -20,8 +20,10 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.riotx.features.login.LoginMode
data class SoftLogoutViewState( data class SoftLogoutViewState(
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized,
val asyncLoginAction: Async<Unit> = Uninitialized, val asyncLoginAction: Async<Unit> = Uninitialized,
val homeServerUrl: String, val homeServerUrl: String,
val userId: String, val userId: String,

View File

@ -55,73 +55,117 @@
tools:visibility="visible" /> tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/softLogoutPasswordContainer" android:id="@+id/softLogoutFormContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"> android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout <!-- Displayed while loading -->
android:id="@+id/softLogoutPasswordFieldTil" <ProgressBar
style="@style/VectorTextInputLayout" android:id="@+id/softLogoutFormLoading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/soft_logout_signin_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/softLogoutPasswordField"
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/softLogoutPasswordReveal"
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>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutForgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:layout_gravity="center_horizontal" />
android:text="@string/auth_forgot_password" />
<!-- Displayed for SSO mode -->
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutSubmit" android:id="@+id/softLogoutFormSsoSubmit"
style="@style/Style.Vector.Login.Button" style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_gravity="center_horizontal"
android:layout_gravity="end" android:text="@string/login_signin_sso"
android:text="@string/soft_logout_signin_submit" android:visibility="gone"
tools:enabled="false" tools:visibility="visible" />
tools:ignore="RelativeOverlap" />
</RelativeLayout> <!-- Displayed in case of error -->
<FrameLayout
android:id="@+id/softLogoutFormError"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/item_error_retry" />
</FrameLayout>
<!-- Displayed for password mode -->
<LinearLayout
android:id="@+id/softLogoutFormPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:id="@+id/softLogoutPasswordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/softLogoutPasswordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/soft_logout_signin_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/softLogoutPasswordField"
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/softLogoutPasswordReveal"
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>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutForgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/auth_forgot_password" />
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:text="@string/soft_logout_signin_submit"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background" android:background="?riotx_background"