diff --git a/changelog.d/5284.wip b/changelog.d/5284.wip new file mode 100644 index 0000000000..a296de24af --- /dev/null +++ b/changelog.d/5284.wip @@ -0,0 +1 @@ +FTUE - Adds support for resetting the password during the FTUE onboarding journey diff --git a/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml b/library/ui-styles/src/main/res/drawable/bg_gradient_ftue_breaker.xml similarity index 100% rename from library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml rename to library/ui-styles/src/main/res/drawable/bg_gradient_ftue_breaker.xml 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 index 41016365c0..40019c5d64 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -19,9 +19,13 @@ package im.vector.app.core.extensions import android.text.Editable import android.view.View import android.view.inputmethod.EditorInfo +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.vector.app.core.platform.SimpleTextWatcher +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.textChanges fun TextInputLayout.editText() = this.editText!! @@ -37,11 +41,18 @@ fun TextInputLayout.content() = editText().text.toString() fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() -fun TextInputLayout.associateContentStateWith(button: View) { +fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) { + editText().textChanges() + .onEach { error = null } + .launchIn(lifecycleOwner.lifecycleScope) +} + +fun TextInputLayout.associateContentStateWith(button: View, enabledPredicate: (String) -> Boolean = { it.isNotEmpty() }) { + button.isEnabled = enabledPredicate(content()) editText().addTextChangedListener(object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { val newContent = s.toString() - button.isEnabled = newContent.isNotEmpty() + button.isEnabled = enabledPredicate(newContent) } }) } 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 b6a7550a58..96b0bc45d6 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 @@ -47,7 +47,9 @@ sealed interface OnboardingAction : VectorViewModelAction { data class LoginWithToken(val loginToken: String) : OnboardingAction data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction - data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction + data class ResetPassword(val email: String, val newPassword: String?) : OnboardingAction + data class ConfirmNewPassword(val newPassword: String, val signOutAllDevices: Boolean) : OnboardingAction + object ResendResetPassword : OnboardingAction object ResetPasswordMailConfirmed : OnboardingAction data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index bf53a72cc3..ea6981a2b5 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -47,9 +47,11 @@ sealed class OnboardingViewEvents : VectorViewEvents { object OnHomeserverEdited : OnboardingViewEvents() data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() object OnForgetPasswordClicked : OnboardingViewEvents() - object OnResetPasswordSendThreePidDone : OnboardingViewEvents() - object OnResetPasswordMailConfirmationSuccess : OnboardingViewEvents() - object OnResetPasswordMailConfirmationSuccessDone : OnboardingViewEvents() + + data class OnResetPasswordEmailConfirmationSent(val email: String) : OnboardingViewEvents() + object OpenResetPasswordComplete : OnboardingViewEvents() + object OnResetPasswordBreakerConfirmed : OnboardingViewEvents() + object OnResetPasswordComplete : OnboardingViewEvents() data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents() data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents() 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 38a72441e0..b5f5682be1 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 @@ -149,6 +149,8 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) + OnboardingAction.ResendResetPassword -> handleResendResetPassword() + is OnboardingAction.ConfirmNewPassword -> handleResetPasswordConfirmed(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) is OnboardingAction.ResetAction -> handleResetAction(action) @@ -439,25 +441,9 @@ class OnboardingViewModel @AssistedInject constructor( } private fun handleResetPassword(action: OnboardingAction.ResetPassword) { - val safeLoginWizard = loginWizard - setState { copy(isLoading = true) } - currentJob = viewModelScope.launch { - runCatching { safeLoginWizard.resetPassword(action.email) }.fold( - onSuccess = { - val state = awaitState() - setState { - copy( - isLoading = false, - resetState = createResetState(action, state.selectedHomeserver) - ) - } - _viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone) - }, - onFailure = { - setState { copy(isLoading = false) } - _viewEvents.post(OnboardingViewEvents.Failure(it)) - } - ) + startResetPasswordFlow(action.email) { + setState { copy(isLoading = false, resetState = createResetState(action, selectedHomeserver)) } + _viewEvents.post(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(action.email)) } } @@ -467,6 +453,41 @@ class OnboardingViewModel @AssistedInject constructor( supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported ) + private fun handleResendResetPassword() { + withState { state -> + val resetState = state.resetState + when (resetState.email) { + null -> _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No reset email has been set"))) + else -> { + startResetPasswordFlow(resetState.email) { + setState { copy(isLoading = false) } + } + } + } + } + } + + private fun startResetPasswordFlow(email: String, onSuccess: suspend () -> Unit) { + val safeLoginWizard = loginWizard + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + runCatching { safeLoginWizard.resetPassword(email) }.fold( + onSuccess = { onSuccess.invoke() }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + ) + } + } + + private fun handleResetPasswordConfirmed(action: OnboardingAction.ConfirmNewPassword) { + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + confirmPasswordReset(action.newPassword, action.signOutAllDevices) + } + } + private fun handleResetPasswordMailConfirmed() { setState { copy(isLoading = true) } currentJob = viewModelScope.launch { @@ -476,27 +497,28 @@ class OnboardingViewModel @AssistedInject constructor( setState { copy(isLoading = false) } _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set"))) } - else -> { - runCatching { loginWizard.resetPasswordMailConfirmed(newPassword) }.fold( - onSuccess = { - setState { - copy( - isLoading = false, - resetState = ResetState() - ) - } - _viewEvents.post(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess) - }, - onFailure = { - setState { copy(isLoading = false) } - _viewEvents.post(OnboardingViewEvents.Failure(it)) - } - ) - } + else -> confirmPasswordReset(newPassword, logoutAllDevices = true) } } } + private suspend fun confirmPasswordReset(newPassword: String, logoutAllDevices: Boolean) { + runCatching { loginWizard.resetPasswordMailConfirmed(newPassword, logoutAllDevices = logoutAllDevices) }.fold( + onSuccess = { + setState { copy(isLoading = false, resetState = ResetState()) } + val nextEvent = when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete + else -> OnboardingViewEvents.OpenResetPasswordComplete + } + _viewEvents.post(nextEvent) + }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + ) + } + private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) { setState { copy(isLoading = true) } currentJob = viewModelScope.launch { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index 205a604aab..d9086952da 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -61,6 +61,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor( views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.loginPasswordInput.setOnImeDoneListener { submit() } views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) } + views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) } } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt index b689f40837..bc44a7dbdb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt @@ -21,8 +21,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import androidx.lifecycle.lifecycleScope import im.vector.app.R +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.realignPercentagesToParent @@ -34,10 +34,7 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.isHomeserverUnavailable -import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment() { @@ -66,9 +63,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt } views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) } views.chooseServerSubmit.debouncedClicks { updateServerUrl() } - views.chooseServerInput.editText().textChanges() - .onEach { views.chooseServerInput.error = null } - .launchIn(viewLifecycleOwner.lifecycleScope) + views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner) } private fun updateServerUrl() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt index ea376709f5..523d576120 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -20,19 +20,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content -import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.databinding.FragmentFtueEmailInputBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.RegisterAction -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.auth.registration.RegisterThreePid -import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment() { @@ -47,16 +43,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen } private fun setupViews() { - views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit) + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() }) views.emailEntryInput.setOnImeDoneListener { updateEmail() } + views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner) views.emailEntrySubmit.debouncedClicks { updateEmail() } - - views.emailEntryInput.editText().textChanges() - .onEach { - views.emailEntryInput.error = null - views.emailEntrySubmit.isEnabled = it.isEmail() - } - .launchIn(viewLifecycleOwner.lifecycleScope) } private fun updateEmail() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt new file mode 100644 index 0000000000..41e24e96c2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt @@ -0,0 +1,70 @@ +/* + * 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.features.onboarding.ftueauth + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.utils.colorTerminatingFullStop +import im.vector.app.databinding.FragmentFtueResetPasswordBreakerBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.themes.ThemeProvider +import im.vector.app.features.themes.ThemeUtils +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class FtueAuthResetPasswordBreakerArgument( + val email: String +) : Parcelable + +@AndroidEntryPoint +class FtueAuthResetPasswordBreakerFragment : AbstractFtueAuthFragment() { + + @Inject lateinit var themeProvider: ThemeProvider + private val params: FtueAuthResetPasswordBreakerArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordBreakerBinding { + return FragmentFtueResetPasswordBreakerBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + } + + private fun setupUi() { + views.resetPasswordBreakerGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground()) + views.resetPasswordBreakerTitle.text = getString(R.string.ftue_auth_reset_password_breaker_title) + .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) + views.resetPasswordBreakerSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) + views.resetPasswordBreakerResendEmail.debouncedClicks { viewModel.handle(OnboardingAction.ResendResetPassword) } + views.resetPasswordBreakerFooter.debouncedClicks { + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordBreakerConfirmed)) + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt new file mode 100644 index 0000000000..e8110569a2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt @@ -0,0 +1,63 @@ +/* + * 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.features.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding +import im.vector.app.features.onboarding.OnboardingAction + +@AndroidEntryPoint +class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordEmailInputBinding { + return FragmentFtueResetPasswordEmailInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() }) + views.emailEntryInput.setOnImeDoneListener { startPasswordReset() } + views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner) + views.emailEntrySubmit.debouncedClicks { startPasswordReset() } + } + + private fun startPasswordReset() { + val email = views.emailEntryInput.content() + viewModel.handle(OnboardingAction.ResetPassword(email = email, newPassword = null)) + } + + override fun onError(throwable: Throwable) { + views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt new file mode 100644 index 0000000000..6282fded61 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt @@ -0,0 +1,78 @@ +/* + * 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.features.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueResetPasswordInputBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState + +@AndroidEntryPoint +class FtueAuthResetPasswordEntryFragment : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordInputBinding { + return FragmentFtueResetPasswordInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.newPasswordInput.associateContentStateWith(button = views.newPasswordSubmit) + views.newPasswordInput.setOnImeDoneListener { resetPassword() } + views.newPasswordInput.clearErrorOnChange(viewLifecycleOwner) + views.newPasswordSubmit.debouncedClicks { resetPassword() } + } + + private fun resetPassword() { + viewModel.handle( + OnboardingAction.ConfirmNewPassword( + newPassword = views.newPasswordInput.content(), + signOutAllDevices = views.entrySignOutAll.isChecked + ) + ) + } + + override fun onError(throwable: Throwable) { + views.newPasswordInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun updateWithState(state: OnboardingViewState) { + views.signedOutAllGroup.isVisible = state.resetState.supportsLogoutAllDevices + + if (state.isLoading) { + views.newPasswordInput.editText().hidePassword() + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt index adc6e1b214..956566a587 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt @@ -41,7 +41,7 @@ class FtueAuthResetPasswordSuccessFragment @Inject constructor() : AbstractFtueA } private fun submit() { - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone)) + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordComplete)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index fa37e2edce..2880b16156 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.os.Parcelable import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.core.view.isVisible @@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.popBackstack @@ -162,30 +162,38 @@ class FtueAuthVariant( ) is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents) is OnboardingViewEvents.OnForgetPasswordClicked -> + when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordEmailEntryFragment::class.java) + else -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordFragment::class.java) + } + is OnboardingViewEvents.OnResetPasswordEmailConfirmationSent -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack( + FtueAuthResetPasswordBreakerFragment::class.java, + FtueAuthResetPasswordBreakerArgument(viewEvents.email), + ) + else -> activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthResetPasswordMailConfirmationFragment::class.java, + ) + } + } + OnboardingViewEvents.OnResetPasswordBreakerConfirmed -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack( views.loginFragmentContainer, - FtueAuthResetPasswordFragment::class.java, - option = commonOption - ) - is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack( - views.loginFragmentContainer, - FtueAuthResetPasswordMailConfirmationFragment::class.java, + FtueAuthResetPasswordEntryFragment::class.java, option = commonOption ) } - is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack( - views.loginFragmentContainer, - FtueAuthResetPasswordSuccessFragment::class.java, - option = commonOption - ) + is OnboardingViewEvents.OpenResetPasswordComplete -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + addLoginStageFragmentToBackstack(FtueAuthResetPasswordSuccessFragment::class.java) } - is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + OnboardingViewEvents.OnResetPasswordComplete -> { + Toast.makeText(activity, R.string.ftue_auth_password_reset_confirmation, Toast.LENGTH_SHORT).show() + activity.popBackstack() } is OnboardingViewEvents.OnSendEmailSuccess -> { openWaitForEmailVerification(viewEvents.email) @@ -496,4 +504,14 @@ class FtueAuthVariant( option = commonOption ) } + + private fun addLoginStageFragmentToBackstack(fragmentClass: Class, params: Parcelable? = null) { + activity.addFragmentToBackstack( + views.loginFragmentContainer, + fragmentClass, + params, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index 75090f2a55..4649c7c799 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -58,12 +58,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor( } private fun setupUi() { - views.emailVerificationGradientContainer.setBackgroundResource( - when (themeProvider.isLightTheme()) { - true -> R.drawable.bg_waiting_for_email_verification - false -> R.drawable.bg_color_background - } - ) + views.emailVerificationGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground()) views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title) .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt index 8d63fbf547..8deb10b7b8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt @@ -18,9 +18,11 @@ package im.vector.app.features.onboarding.ftueauth import android.widget.Button import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R import im.vector.app.core.extensions.hasContentFlow import im.vector.app.features.login.SignMode import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.themes.ThemeProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach @@ -49,3 +51,8 @@ fun observeContentChangesAndResetErrors(username: TextInputLayout, password: Tex submit.isEnabled = it } } + +fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) { + true -> R.drawable.bg_gradient_ftue_breaker + false -> R.drawable.bg_color_background +} diff --git a/vector/src/main/res/drawable/ic_new_password.xml b/vector/src/main/res/drawable/ic_new_password.xml new file mode 100644 index 0000000000..b6171642c8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_new_password.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml index 8037f207fc..d50fdb6394 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -170,7 +170,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:hint="@string/login_signup_password_hint" - app:layout_constraintBottom_toTopOf="@id/actionSpacing" + app:layout_constraintBottom_toTopOf="@id/loginForgotPassword" app:layout_constraintEnd_toEndOf="@id/loginGutterEnd" app:layout_constraintStart_toStartOf="@id/loginGutterStart" app:layout_constraintTop_toBottomOf="@id/entrySpacing"> @@ -184,13 +184,27 @@ +