Login screens: move user choices to the ViewState

This commit is contained in:
Benoit Marty 2019-11-26 17:59:01 +01:00
parent 7ce8a13ddf
commit d5c2c1938c
14 changed files with 186 additions and 186 deletions

View File

@ -21,6 +21,7 @@ import android.view.View
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
@ -36,6 +37,8 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
protected val loginViewModel: LoginViewModel by activityViewModel()
protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
private var isResetPasswordStarted = false
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -53,7 +56,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.Error -> showError(loginViewEvents.throwable)
else ->
else ->
// This is handled by the Activity
Unit
}
@ -81,7 +84,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {
loginViewModel.isRegistrationStarted -> {
loginViewModel.isRegistrationStarted -> {
// Ask for confirmation before cancelling the registration
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_signup_cancel_confirmation_title)
@ -95,7 +98,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
true
}
loginViewModel.isResetPasswordStarted -> {
isResetPasswordStarted -> {
// Ask for confirmation before cancelling the reset password
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_reset_password_cancel_confirmation_title)
@ -109,7 +112,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
true
}
else -> {
else -> {
resetViewModel()
// Do not consume the Back event
false
@ -117,6 +120,17 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
}
}
final override fun invalidate() = withState(loginViewModel) { state ->
// True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
updateWithState(state)
}
open fun updateWithState(state: LoginViewState) {
// No op by default
}
// Reset any modification on the loginViewModel by the current fragment
abstract fun resetViewModel()
}

View File

@ -24,16 +24,17 @@ sealed class LoginAction : VectorViewModelAction {
data class UpdateServerType(val serverType: ServerType) : LoginAction()
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
data class Login(val login: String, val password: String, val initialDeviceName: String) : LoginAction()
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
data class ResetPassword(val email: String, val newPassword: String) : LoginAction()
object ResetPasswordMailConfirmed : LoginAction()
// Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : LoginAction()
// Register actions
open class RegisterAction : LoginAction()
data class RegisterWith(val username: String, val password: String, val initialDeviceName: String) : RegisterAction()
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
object SendAgainThreePid : RegisterAction()
// TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX)

View File

@ -196,29 +196,27 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
.show()
}
private fun onServerSelectionDone() {
when (loginViewModel.serverType) {
private fun onServerSelectionDone() = withState(loginViewModel) { state ->
when (state.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.Modular,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java)
}
}
private fun onSignModeSelected() {
when (loginViewModel.signMode) {
private fun onSignModeSelected() = withState(loginViewModel) { state ->
when (state.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> {
// This is managed by the LoginViewEvents
}
SignMode.SignIn -> {
// It depends on the LoginMode
withState(loginViewModel) {
when (val loginMode = it.asyncHomeServerLoginFlowRequest.invoke()) {
null -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java)
is LoginMode.Unsupported -> onLoginModeNotSupported(loginMode)
}
when (val loginMode = state.asyncHomeServerLoginFlowRequest.invoke()) {
null -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java)
is LoginMode.Unsupported -> onLoginModeNotSupported(loginMode)
}
}
}

View File

@ -21,10 +21,8 @@ import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
import android.view.View
import android.webkit.*
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
@ -57,14 +55,10 @@ class LoginCaptchaFragment @Inject constructor(
private val params: LoginCaptchaFragmentArgument by args()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWebView()
}
private var isWebViewLoaded = false
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
private fun setupWebView(state: LoginViewState) {
loginCaptchaWevView.settings.javaScriptEnabled = true
val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html")
@ -73,7 +67,7 @@ class LoginCaptchaFragment @Inject constructor(
val mime = "text/html"
val encoding = "utf-8"
val homeServerUrl = loginViewModel.homeServerUrl ?: error("missing url of homeserver")
val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver")
loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null)
loginCaptchaWevView.requestLayout()
@ -189,4 +183,11 @@ class LoginCaptchaFragment @Inject constructor(
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun updateWithState(state: LoginViewState) {
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
}

View File

@ -25,7 +25,6 @@ import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
@ -57,16 +56,13 @@ class LoginFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
setupAutoFill()
setupSubmitButton()
setupPasswordReveal()
setupButtons()
}
private fun setupAutoFill() {
private fun setupAutoFill(state: LoginViewState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (loginViewModel.signMode) {
when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
@ -87,11 +83,7 @@ class LoginFragment @Inject constructor(
val login = loginField.text.toString()
val password = passwordField.text.toString()
when (loginViewModel.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> loginViewModel.handle(LoginAction.RegisterWith(login, password, getString(R.string.login_mobile_device)))
SignMode.SignIn -> loginViewModel.handle(LoginAction.Login(login, password, getString(R.string.login_mobile_device)))
}
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device)))
}
private fun cleanupUi() {
@ -100,18 +92,18 @@ class LoginFragment @Inject constructor(
passwordFieldTil.error = null
}
private fun setupUi() {
val resId = when (loginViewModel.signMode) {
private fun setupUi(state: LoginViewState) {
val resId = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_to
SignMode.SignIn -> R.string.login_connect_to
}
when (loginViewModel.serverType) {
when (state.serverType) {
ServerType.MatrixOrg -> {
loginServerIcon.isVisible = true
loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
loginTitle.text = getString(resId, loginViewModel.getHomeServerUrlSimple())
loginTitle.text = getString(resId, state.homeServerUrlSimple)
loginNotice.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.Modular -> {
@ -123,16 +115,16 @@ class LoginFragment @Inject constructor(
}
ServerType.Other -> {
loginServerIcon.isVisible = false
loginTitle.text = getString(resId, loginViewModel.getHomeServerUrlSimple())
loginTitle.text = getString(resId, state.homeServerUrlSimple)
loginNotice.text = getString(R.string.login_server_other_text)
}
}
}
private fun setupButtons() {
forgetPasswordButton.isVisible = loginViewModel.signMode == SignMode.SignIn
private fun setupButtons(state: LoginViewState) {
forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn
loginSubmit.text = getString(when (loginViewModel.signMode) {
loginSubmit.text = getString(when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_submit
SignMode.SignIn -> R.string.login_signin
@ -193,7 +185,11 @@ class LoginFragment @Inject constructor(
loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun invalidate() = withState(loginViewModel) { state ->
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupAutoFill(state)
setupButtons(state)
when (state.asyncLoginAction) {
is Loading -> {
// Ensure password is hidden

View File

@ -23,7 +23,6 @@ import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
@ -53,13 +52,12 @@ class LoginResetPasswordFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
setupSubmitButton()
setupPasswordReveal()
}
private fun setupUi() {
resetPasswordTitle.text = getString(R.string.login_reset_password_on, loginViewModel.getHomeServerUrlSimple())
private fun setupUi(state: LoginViewState) {
resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple)
}
private fun setupSubmitButton() {
@ -148,7 +146,9 @@ class LoginResetPasswordFragment @Inject constructor(
.show()
}
override fun invalidate() = withState(loginViewModel) { state ->
override fun updateWithState(state: LoginViewState) {
setupUi(state)
when (state.asyncResetPassword) {
is Loading -> {
// Ensure new password is hidden

View File

@ -16,13 +16,10 @@
package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
@ -38,14 +35,8 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor(
override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
}
private fun setupUi() {
resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, loginViewModel.resetPasswordEmail)
private fun setupUi(state: LoginViewState) {
resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
}
@OnClick(R.id.resetPasswordMailConfirmationSubmit)
@ -65,7 +56,9 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetResetPassword)
}
override fun invalidate() = withState(loginViewModel) { state ->
override fun updateWithState(state: LoginViewState) {
setupUi(state)
when (state.asyncResetMailConfirmed) {
is Fail -> {
// Link in email not yet clicked ?
@ -85,7 +78,5 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess)
}
}
Unit
}
}

View File

@ -22,7 +22,6 @@ import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.transition.TransitionInflater
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import im.vector.riotx.R
@ -52,12 +51,11 @@ class LoginServerSelectionFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateSelectedChoice()
initTextViews()
}
private fun updateSelectedChoice() {
loginViewModel.serverType.let {
private fun updateSelectedChoice(state: LoginViewState) {
state.serverType.let {
loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg
loginServerChoiceModular.isChecked = it == ServerType.Modular
loginServerChoiceOther.isChecked = it == ServerType.Other
@ -83,7 +81,6 @@ class LoginServerSelectionFragment @Inject constructor(
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
updateSelectedChoice()
}
}
@ -94,7 +91,6 @@ class LoginServerSelectionFragment @Inject constructor(
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
updateSelectedChoice()
}
}
@ -105,13 +101,12 @@ class LoginServerSelectionFragment @Inject constructor(
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
updateSelectedChoice()
}
}
@OnClick(R.id.loginServerSubmit)
fun submit() {
if (loginViewModel.serverType == ServerType.MatrixOrg) {
fun submit() = withState(loginViewModel) { state ->
if (state.serverType == ServerType.MatrixOrg) {
// Request login flow here
loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url)))
} else {
@ -131,11 +126,10 @@ class LoginServerSelectionFragment @Inject constructor(
.show()
}
override fun invalidate() = withState(loginViewModel) {
when (it.asyncHomeServerLoginFlowRequest) {
is Fail -> {
// TODO Display error in a dialog?
}
override fun updateWithState(state: LoginViewState) {
updateSelectedChoice(state)
when (state.asyncHomeServerLoginFlowRequest) {
is Success -> {
// LoginFlow for matrix.org has been retrieved
loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved)

View File

@ -23,7 +23,6 @@ import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
@ -44,7 +43,6 @@ class LoginServerUrlFormFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
setupHomeServerField()
}
@ -65,8 +63,8 @@ class LoginServerUrlFormFragment @Inject constructor(
}
}
private fun setupUi() {
when (loginViewModel.serverType) {
private fun setupUi(state: LoginViewState) {
when (state.serverType) {
ServerType.Modular -> {
loginServerUrlFormIcon.isVisible = true
loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular)
@ -127,7 +125,9 @@ class LoginServerUrlFormFragment @Inject constructor(
loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun invalidate() = withState(loginViewModel) { state ->
override fun updateWithState(state: LoginViewState) {
setupUi(state)
when (state.asyncHomeServerLoginFlowRequest) {
is Success -> {
// The home server url is valid

View File

@ -16,12 +16,9 @@
package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
@ -38,21 +35,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
private var isSsoSignIn: Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isSsoSignIn = withState(loginViewModel) { it.asyncHomeServerLoginFlowRequest.invoke() } == LoginMode.Sso
setupUi()
setupButtons()
}
private fun setupUi() {
when (loginViewModel.serverType) {
private fun setupUi(state: LoginViewState) {
when (state.serverType) {
ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
loginSignupSigninServerIcon.isVisible = true
loginSignupSigninTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple())
loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.Modular -> {
@ -60,17 +48,19 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
loginSignupSigninServerIcon.isVisible = true
// TODO
loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME")
loginSignupSigninText.text = loginViewModel.getHomeServerUrlSimple()
loginSignupSigninText.text = state.homeServerUrlSimple
}
ServerType.Other -> {
loginSignupSigninServerIcon.isVisible = false
loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
loginSignupSigninText.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple())
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
}
}
}
private fun setupButtons() {
private fun setupButtons(state: LoginViewState) {
isSsoSignIn = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso
if (isSsoSignIn) {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
@ -106,4 +96,9 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetSignMode)
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons(state)
}
}

View File

@ -73,24 +73,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
var isRegistrationStarted: Boolean = false
private set
// True when email is sent with success to the homeserver
val isResetPasswordStarted: Boolean
get() = resetPasswordEmail.isNullOrBlank().not()
private var registrationWizard: RegistrationWizard? = null
private var loginWizard: LoginWizard? = null
// TODO Move all this in a data class
var serverType: ServerType = ServerType.MatrixOrg
private set
var signMode: SignMode = SignMode.Unknown
private set
var resetPasswordEmail: String? = null
private set
var homeServerUrl: String? = null
private set
private var loginConfig: LoginConfig? = null
private var currentTask: Cancelable? = null
@ -104,7 +89,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
is LoginAction.Login -> handleLogin(action)
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action)
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
@ -115,7 +100,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private fun handleRegisterAction(action: LoginAction.RegisterAction) {
when (action) {
is LoginAction.RegisterWith -> handleRegisterWith(action)
is LoginAction.CaptchaDone -> handleCaptchaDone(action)
is LoginAction.AcceptTerms -> handleAcceptTerms()
is LoginAction.RegisterDummy -> handleRegisterDummy()
@ -232,7 +216,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
currentTask = registrationWizard?.dummy(registrationCallback)
}
private fun handleRegisterWith(action: LoginAction.RegisterWith) {
private fun handleRegisterWith(action: LoginAction.LoginOrRegister) {
setState { copy(asyncRegistration = Loading()) }
currentTask = registrationWizard?.createAccount(
action.username,
@ -270,33 +254,37 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
LoginAction.ResetHomeServerUrl -> {
homeServerUrl = null
registrationWizard = null
loginWizard = null
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = null
)
}
}
LoginAction.ResetHomeServerType -> {
serverType = ServerType.MatrixOrg
}
LoginAction.ResetSignMode -> {
signMode = SignMode.Unknown
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
serverType = ServerType.MatrixOrg
)
}
}
LoginAction.ResetSignMode -> {
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
signMode = SignMode.Unknown
)
}
}
LoginAction.ResetResetPassword -> {
resetPasswordEmail = null
setState {
copy(
asyncResetPassword = Uninitialized,
asyncResetMailConfirmed = Uninitialized
asyncResetMailConfirmed = Uninitialized,
resetPasswordEmail = null
)
}
}
@ -304,17 +292,25 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) {
signMode = action.signMode
setState {
copy(
signMode = action.signMode
)
}
if (signMode == SignMode.SignUp) {
if (action.signMode == SignMode.SignUp) {
startRegistrationFlow()
} else if (signMode == SignMode.SignIn) {
} else if (action.signMode == SignMode.SignIn) {
startAuthenticationFlow()
}
}
private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
serverType = action.serverType
setState {
copy(
serverType = action.serverType
)
}
}
private fun handleInitWith(action: LoginAction.InitWith) {
@ -339,11 +335,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
currentTask = safeLoginWizard.resetPassword(action.email, action.newPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
resetPasswordEmail = action.email
setState {
copy(
asyncResetPassword = Success(data)
asyncResetPassword = Success(data),
resetPasswordEmail = action.email
)
}
}
@ -378,11 +373,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
currentTask = safeLoginWizard.resetPasswordMailConfirmed(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
resetPasswordEmail = null
setState {
copy(
asyncResetMailConfirmed = Success(data)
asyncResetMailConfirmed = Success(data),
resetPasswordEmail = null
)
}
}
@ -399,7 +393,15 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
private fun handleLogin(action: LoginAction.Login) {
private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state ->
when (state.signMode) {
SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action)
else -> error("Developer error, invalid sign mode")
}
}
private fun handleLogin(action: LoginAction.LoginOrRegister) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
@ -416,7 +418,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
currentTask = safeLoginWizard.login(
action.login,
action.username,
action.password,
action.initialDeviceName,
object : MatrixCallback<Session> {
@ -473,8 +475,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
private fun handleWebLoginSuccess(action: LoginAction.WebLoginSuccess) {
val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(homeServerUrl)
private fun handleWebLoginSuccess(action: LoginAction.WebLoginSuccess) = withState { state ->
val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl)
if (homeServerConnectionConfigFinal == null) {
// Should not happen
@ -525,9 +527,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
override fun onSuccess(data: LoginFlowResult) {
// Keep the url
homeServerUrl = action.homeServerUrl
when (data) {
is LoginFlowResult.Success -> {
val loginMode = when {
@ -542,7 +541,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
} else {
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
asyncHomeServerLoginFlowRequest = Success(loginMode),
homeServerUrl = action.homeServerUrl
)
}
}
@ -576,13 +576,4 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
fun getInitialHomeServerUrl(): String? {
return loginConfig?.homeServerUrl
}
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
fun getHomeServerUrlSimple(): String {
return (homeServerUrl ?: "")
.substringAfter("://")
.trim { it == '/' }
}
}

View File

@ -23,7 +23,17 @@ data class LoginViewState(
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized,
val asyncResetPassword: Async<Unit> = Uninitialized,
val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
val asyncRegistration: Async<Unit> = Uninitialized
val asyncRegistration: Async<Unit> = Uninitialized,
// User choice
@PersistState
val serverType: ServerType = ServerType.MatrixOrg,
@PersistState
val signMode: SignMode = SignMode.Unknown,
@PersistState
val resetPasswordEmail: String? = null,
@PersistState
val homeServerUrl: String? = null
) : MvRxState {
fun isLoading(): Boolean {
@ -37,4 +47,12 @@ data class LoginViewState(
fun isUserLogged(): Boolean {
return asyncLoginAction is Success
}
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
val homeServerUrlSimple: String
get() = (homeServerUrl ?: "")
.substringAfter("://")
.trim { it == '/' }
}

View File

@ -48,50 +48,48 @@ class LoginWebFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
private lateinit var homeServerUrl: String
private lateinit var signMode: SignMode
override fun getLayoutResId() = R.layout.fragment_login_web
private var isWebViewLoaded = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeServerUrl = loginViewModel.homeServerUrl ?: error("Developer error: Invalid url")
signMode = loginViewModel.signMode.takeIf { it != SignMode.Unknown } ?: error("Developer error: Invalid sign mode")
setupToolbar(loginWebToolbar)
setupTitle()
setupWebView()
}
private fun setupTitle() {
loginWebToolbar.title = when (signMode) {
override fun updateWithState(state: LoginViewState) {
setupTitle(state)
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
private fun setupTitle(state: LoginViewState) {
loginWebToolbar.title = when (state.signMode) {
SignMode.SignIn -> getString(R.string.login_signin)
else -> getString(R.string.login_signup)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
private fun setupWebView(state: LoginViewState) {
loginWebWebView.settings.javaScriptEnabled = true
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google"
if (!homeServerUrl.endsWith("/")) {
homeServerUrl += "/"
}
// AppRTC requires third party cookies to work
val cookieManager = android.webkit.CookieManager.getInstance()
// clear the cookies must be cleared
if (cookieManager == null) {
launchWebView()
launchWebView(state)
} else {
if (!cookieManager.hasCookies()) {
launchWebView()
launchWebView(state)
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
try {
cookieManager.removeAllCookie()
@ -99,24 +97,24 @@ class LoginWebFragment @Inject constructor(
Timber.e(e, " cookieManager.removeAllCookie() fails")
}
launchWebView()
launchWebView(state)
} else {
try {
cookieManager.removeAllCookies { launchWebView() }
cookieManager.removeAllCookies { launchWebView(state) }
} catch (e: Exception) {
Timber.e(e, " cookieManager.removeAllCookie() fails")
launchWebView()
launchWebView(state)
}
}
}
}
private fun launchWebView() {
if (signMode == SignMode.SignIn) {
loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/login/")
private fun launchWebView(state: LoginViewState) {
if (state.signMode == SignMode.SignIn) {
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/")
} else {
// MODE_REGISTER
loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/register/")
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/")
}
loginWebWebView.webViewClient = object : WebViewClient() {
@ -157,7 +155,7 @@ class LoginWebFragment @Inject constructor(
val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js")
view.loadUrl(mxcJavascriptSendObjectMessage)
if (signMode == SignMode.SignIn) {
if (state.signMode == SignMode.SignIn) {
// The function the fallback page calls when the login is complete
val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js")
view.loadUrl(mxcJavascriptOnLogin)
@ -211,7 +209,7 @@ class LoginWebFragment @Inject constructor(
if (javascriptResponse != null) {
val action = javascriptResponse.action
if (signMode == SignMode.SignIn) {
if (state.signMode == SignMode.SignIn) {
try {
if (action == "onLogin") {
val credentials = javascriptResponse.credentials

View File

@ -27,6 +27,7 @@ import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
import im.vector.riotx.features.login.LoginViewState
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_terms.*
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
@ -56,7 +57,6 @@ class LoginTermsFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
loginTermsPolicyList.setController(policyController)
policyController.homeServer = loginViewModel.getHomeServerUrlSimple()
policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
@ -67,8 +67,6 @@ class LoginTermsFragment @Inject constructor(
}
loginTermsViewState = LoginTermsViewState(list)
renderState()
}
private fun renderState() {
@ -109,6 +107,11 @@ class LoginTermsFragment @Inject constructor(
.show()
}
override fun updateWithState(state: LoginViewState) {
policyController.homeServer = state.homeServerUrlSimple
renderState()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}