From 585ac4bf1f6d16d660f68d6a1d5c791a9ebeeb43 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Mar 2022 11:21:14 +0100 Subject: [PATCH] extracting common textinputlayer interactions to their own extensions and providing a dedicated register action --- .../app/core/extensions/TextInputLayout.kt | 32 +++++++++++++ .../features/onboarding/OnboardingAction.kt | 1 + .../onboarding/OnboardingViewModel.kt | 5 ++- .../FtueAuthCombinedSignUpFragment.kt | 45 +++++++++---------- 4 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt new file mode 100644 index 0000000000..4347da71f0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.core.extensions + +import com.google.android.material.textfield.TextInputLayout +import kotlinx.coroutines.flow.map +import reactivecircus.flowbinding.android.widget.textChanges + +fun TextInputLayout.editText() = this.editText!! + +/** + * Detect if a field starts or ends with spaces + */ +fun TextInputLayout.hasSurroundingSpaces() = editText().text.toString().let { it.trim() != it } + +fun TextInputLayout.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() } + +fun TextInputLayout.content() = editText().text.toString() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index 7fa75d1544..83112d380a 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -41,6 +41,7 @@ sealed interface OnboardingAction : VectorViewModelAction { // Login or Register, depending on the signMode data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction + data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction object StopEmailValidationCheck : OnboardingAction data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 33778b7224..e9c33d18d6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -141,6 +141,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.InitWith -> handleInitWith(action) is OnboardingAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } + is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action } is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) @@ -276,7 +277,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { + private fun handleRegisterWith(action: OnboardingAction.Register) { reAuthHelper.data = action.password handleRegisterAction(RegisterAction.CreateAccount( action.username, @@ -465,7 +466,7 @@ class OnboardingViewModel @AssistedInject constructor( when (state.signMode) { SignMode.Unknown -> error("Developer error, invalid sign mode") SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName)) SignMode.SignInWithMatrixId -> handleDirectLogin(action, null) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedSignUpFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedSignUpFragment.kt index c52c59ee72..31f7cd9794 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedSignUpFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedSignUpFragment.kt @@ -28,8 +28,11 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputLayout import im.vector.app.R +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.hasContentFlow +import im.vector.app.core.extensions.hasSurroundingSpaces import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent @@ -42,7 +45,6 @@ import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername @@ -50,7 +52,6 @@ import org.matrix.android.sdk.api.failure.isLoginEmailUnknown import org.matrix.android.sdk.api.failure.isRegistrationDisabled import org.matrix.android.sdk.api.failure.isUsernameInUse import org.matrix.android.sdk.api.failure.isWeakPassword -import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuthFragment() { @@ -91,8 +92,8 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth cleanupUi() - val login = views.createAccountInput.editText.toString() - val password = views.createAccountPasswordInput.editText.toString() + val login = views.createAccountInput.content() + val password = views.createAccountPasswordInput.content() // This can be called by the IME action, so deal with empty cases var error = 0 @@ -110,7 +111,7 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth } if (error == 0) { - viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name))) + viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name))) } } } @@ -143,12 +144,7 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth private fun setupSubmitButton() { views.createAccountSubmit.setOnClickListener { submit() } - combine( - views.createAccountInput.editText().textChanges().map { it.trim().isNotEmpty() }, - views.createAccountPasswordInput.editText().textChanges().map { it.isNotEmpty() } - ) { isLoginNotEmpty, isPasswordNotEmpty -> - isLoginNotEmpty && isPasswordNotEmpty - } + observeInputFields() .onEach { views.createAccountPasswordInput.error = null views.createAccountInput.error = null @@ -157,6 +153,12 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth .launchIn(viewLifecycleOwner.lifecycleScope) } + private fun observeInputFields() = combine( + views.createAccountInput.hasContentFlow { it.trim() }, + views.createAccountPasswordInput.hasContentFlow(), + transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty } + ) + override fun resetViewModel() { viewModel.handle(OnboardingAction.ResetLogin) } @@ -165,26 +167,26 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth // Trick to display the error without text. views.createAccountInput.error = " " when { - throwable.isUsernameInUse() || throwable.isInvalidUsername() -> { + throwable.isUsernameInUse() || throwable.isInvalidUsername() -> { views.createAccountInput.error = errorFormatter.toHumanReadable(throwable) } - throwable.isLoginEmailUnknown() -> { + throwable.isLoginEmailUnknown() -> { views.createAccountInput.error = getString(R.string.login_login_with_email_error) } - throwable.isInvalidPassword() && spaceInPassword() -> { + throwable.isInvalidPassword() && views.createAccountPasswordInput.hasSurroundingSpaces() -> { views.createAccountPasswordInput.error = getString(R.string.auth_invalid_login_param_space_in_password) } - throwable.isWeakPassword() || throwable.isInvalidPassword() -> { + throwable.isWeakPassword() || throwable.isInvalidPassword() -> { views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable) } - throwable.isRegistrationDisabled() -> { + throwable.isRegistrationDisabled() -> { MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.login_registration_disabled)) .setPositiveButton(R.string.ok, null) .show() } - else -> { + else -> { super.onError(throwable) } } @@ -200,13 +202,6 @@ class FtueAuthCombinedSignUpFragment @Inject constructor() : AbstractSSOFtueAuth views.createAccountPasswordInput.editText().hidePassword() } } - - /** - * Detect if password ends or starts with spaces - */ - private fun spaceInPassword() = views.createAccountPasswordInput.editText().text.toString().let { it.trim() != it } } private fun OnboardingViewState.isNumericOnlyUserIdForbidden() = serverType == ServerType.MatrixOrg - -private fun TextInputLayout.editText() = this.editText!!