Merge pull request #6263 from vector-im/feature/adm/ftue-forgot-password

[FTUE] Forgot password
This commit is contained in:
Adam Brown 2022-06-30 15:40:09 +01:00 committed by GitHub
commit 72c4af0026
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 856 additions and 95 deletions

1
changelog.d/5284.wip Normal file
View file

@ -0,0 +1 @@
FTUE - Adds support for resetting the password during the FTUE onboarding journey

View file

@ -19,9 +19,13 @@ package im.vector.app.core.extensions
import android.text.Editable import android.text.Editable
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.widget.textChanges import reactivecircus.flowbinding.android.widget.textChanges
fun TextInputLayout.editText() = this.editText!! fun TextInputLayout.editText() = this.editText!!
@ -37,11 +41,18 @@ fun TextInputLayout.content() = editText().text.toString()
fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() 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() { editText().addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
val newContent = s.toString() val newContent = s.toString()
button.isEnabled = newContent.isNotEmpty() button.isEnabled = enabledPredicate(newContent)
} }
}) })
} }

View file

@ -47,7 +47,9 @@ sealed interface OnboardingAction : VectorViewModelAction {
data class LoginWithToken(val loginToken: String) : OnboardingAction data class LoginWithToken(val loginToken: String) : OnboardingAction
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
data class InitWith(val loginConfig: LoginConfig?) : 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 object ResetPasswordMailConfirmed : OnboardingAction
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction

View file

@ -47,9 +47,11 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OnHomeserverEdited : OnboardingViewEvents() object OnHomeserverEdited : OnboardingViewEvents()
data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents()
object OnForgetPasswordClicked : OnboardingViewEvents() object OnForgetPasswordClicked : OnboardingViewEvents()
object OnResetPasswordSendThreePidDone : OnboardingViewEvents()
object OnResetPasswordMailConfirmationSuccess : OnboardingViewEvents() data class OnResetPasswordEmailConfirmationSent(val email: String) : OnboardingViewEvents()
object OnResetPasswordMailConfirmationSuccessDone : OnboardingViewEvents() object OpenResetPasswordComplete : OnboardingViewEvents()
object OnResetPasswordBreakerConfirmed : OnboardingViewEvents()
object OnResetPasswordComplete : OnboardingViewEvents()
data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents() data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents()
data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents() data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()

View file

@ -149,6 +149,8 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPassword -> handleResetPassword(action)
OnboardingAction.ResendResetPassword -> handleResendResetPassword()
is OnboardingAction.ConfirmNewPassword -> handleResetPasswordConfirmed(action)
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.ResetAction -> handleResetAction(action)
@ -439,25 +441,9 @@ class OnboardingViewModel @AssistedInject constructor(
} }
private fun handleResetPassword(action: OnboardingAction.ResetPassword) { private fun handleResetPassword(action: OnboardingAction.ResetPassword) {
val safeLoginWizard = loginWizard startResetPasswordFlow(action.email) {
setState { copy(isLoading = true) } setState { copy(isLoading = false, resetState = createResetState(action, selectedHomeserver)) }
currentJob = viewModelScope.launch { _viewEvents.post(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(action.email))
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))
}
)
} }
} }
@ -467,6 +453,41 @@ class OnboardingViewModel @AssistedInject constructor(
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported 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() { private fun handleResetPasswordMailConfirmed() {
setState { copy(isLoading = true) } setState { copy(isLoading = true) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
@ -476,27 +497,28 @@ class OnboardingViewModel @AssistedInject constructor(
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set"))) _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set")))
} }
else -> { else -> confirmPasswordReset(newPassword, logoutAllDevices = true)
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))
}
)
}
} }
} }
} }
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?) { private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
setState { copy(isLoading = true) } setState { copy(isLoading = true) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {

View file

@ -61,6 +61,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
views.loginPasswordInput.setOnImeDoneListener { submit() } views.loginPasswordInput.setOnImeDoneListener { submit() }
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) } views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) }
} }
private fun setupSubmitButton() { private fun setupSubmitButton() {

View file

@ -21,8 +21,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.lifecycle.lifecycleScope
import im.vector.app.R 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.content
import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.realignPercentagesToParent 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.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState 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 org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject import javax.inject.Inject
class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() { class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
@ -66,9 +63,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
} }
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) } views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
views.chooseServerSubmit.debouncedClicks { updateServerUrl() } views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
views.chooseServerInput.editText().textChanges() views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner)
.onEach { views.chooseServerInput.error = null }
.launchIn(viewLifecycleOwner.lifecycleScope)
} }
private fun updateServerUrl() { private fun updateServerUrl() {

View file

@ -20,19 +20,15 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import im.vector.app.core.extensions.associateContentStateWith 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.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.databinding.FragmentFtueEmailInputBinding import im.vector.app.databinding.FragmentFtueEmailInputBinding
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.RegisterAction 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 org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject import javax.inject.Inject
class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueEmailInputBinding>() { class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueEmailInputBinding>() {
@ -47,16 +43,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
} }
private fun setupViews() { private fun setupViews() {
views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit) views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() })
views.emailEntryInput.setOnImeDoneListener { updateEmail() } views.emailEntryInput.setOnImeDoneListener { updateEmail() }
views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner)
views.emailEntrySubmit.debouncedClicks { updateEmail() } views.emailEntrySubmit.debouncedClicks { updateEmail() }
views.emailEntryInput.editText().textChanges()
.onEach {
views.emailEntryInput.error = null
views.emailEntrySubmit.isEnabled = it.isEmail()
}
.launchIn(viewLifecycleOwner.lifecycleScope)
} }
private fun updateEmail() { private fun updateEmail() {

View file

@ -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<FragmentFtueResetPasswordBreakerBinding>() {
@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)
}
}

View file

@ -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<FragmentFtueResetPasswordEmailInputBinding>() {
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)
}
}

View file

@ -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<FragmentFtueResetPasswordInputBinding>() {
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)
}
}

View file

@ -41,7 +41,7 @@ class FtueAuthResetPasswordSuccessFragment @Inject constructor() : AbstractFtueA
} }
private fun submit() { private fun submit() {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone)) viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordComplete))
} }
override fun resetViewModel() { override fun resetViewModel() {

View file

@ -20,6 +20,7 @@ import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentTransaction
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R 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.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.popBackstack import im.vector.app.core.extensions.popBackstack
@ -162,30 +162,38 @@ class FtueAuthVariant(
) )
is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents) is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents)
is OnboardingViewEvents.OnForgetPasswordClicked -> 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( activity.addFragmentToBackstack(
views.loginFragmentContainer, views.loginFragmentContainer,
FtueAuthResetPasswordFragment::class.java, FtueAuthResetPasswordEntryFragment::class.java,
option = commonOption
)
is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
activity.addFragmentToBackstack(
views.loginFragmentContainer,
FtueAuthResetPasswordMailConfirmationFragment::class.java,
option = commonOption option = commonOption
) )
} }
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> { is OnboardingViewEvents.OpenResetPasswordComplete -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.addFragmentToBackstack( addLoginStageFragmentToBackstack(FtueAuthResetPasswordSuccessFragment::class.java)
views.loginFragmentContainer,
FtueAuthResetPasswordSuccessFragment::class.java,
option = commonOption
)
} }
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { OnboardingViewEvents.OnResetPasswordComplete -> {
// Go back to the login fragment Toast.makeText(activity, R.string.ftue_auth_password_reset_confirmation, Toast.LENGTH_SHORT).show()
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) activity.popBackstack()
} }
is OnboardingViewEvents.OnSendEmailSuccess -> { is OnboardingViewEvents.OnSendEmailSuccess -> {
openWaitForEmailVerification(viewEvents.email) openWaitForEmailVerification(viewEvents.email)
@ -496,4 +504,14 @@ class FtueAuthVariant(
option = commonOption option = commonOption
) )
} }
private fun addLoginStageFragmentToBackstack(fragmentClass: Class<out Fragment>, params: Parcelable? = null) {
activity.addFragmentToBackstack(
views.loginFragmentContainer,
fragmentClass,
params,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption
)
}
} }

View file

@ -58,12 +58,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
} }
private fun setupUi() { private fun setupUi() {
views.emailVerificationGradientContainer.setBackgroundResource( views.emailVerificationGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground())
when (themeProvider.isLightTheme()) {
true -> R.drawable.bg_waiting_for_email_verification
false -> R.drawable.bg_color_background
}
)
views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title) views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title)
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email)

View file

@ -18,9 +18,11 @@ package im.vector.app.features.onboarding.ftueauth
import android.widget.Button import android.widget.Button
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.extensions.hasContentFlow import im.vector.app.core.extensions.hasContentFlow
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.themes.ThemeProvider
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -49,3 +51,8 @@ fun observeContentChangesAndResetErrors(username: TextInputLayout, password: Tex
submit.isEnabled = it submit.isEnabled = it
} }
} }
fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) {
true -> R.drawable.bg_gradient_ftue_breaker
false -> R.drawable.bg_color_background
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="70"
android:viewportHeight="70">
<path
android:pathData="M30.125,16.213C27.088,16.213 24.625,18.676 24.625,21.713V31.527H21.625C19.968,31.527 18.625,32.87 18.625,34.527V53.125C18.625,54.782 19.968,56.125 21.625,56.125H49.375C51.032,56.125 52.375,54.782 52.375,53.125V34.527C52.375,32.87 51.032,31.527 49.375,31.527H46.375V21.713C46.375,18.676 43.913,16.213 40.875,16.213H30.125ZM43.375,31.527V21.713C43.375,20.333 42.256,19.213 40.875,19.213H30.125C28.744,19.213 27.625,20.333 27.625,21.713V31.527H43.375Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -170,7 +170,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint" 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_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart" app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing"> app:layout_constraintTop_toBottomOf="@id/entrySpacing">
@ -184,13 +184,27 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/loginForgotPassword"
style="@style/Widget.Vector.Button.Text.Login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ftue_auth_forgot_password"
android:textAllCaps="true"
android:textColor="?colorSecondary"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
<Space <Space
android:id="@+id/actionSpacing" android:id="@+id/actionSpacing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginSubmit" app:layout_constraintBottom_toTopOf="@id/loginSubmit"
app:layout_constraintHeight_percent="0.02" app:layout_constraintHeight_percent="0.02"
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" /> app:layout_constraintTop_toBottomOf="@id/loginForgotPassword" />
<Button <Button
android:id="@+id/loginSubmit" android:id="@+id/loginSubmit"

View file

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/ftueAuthGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/ftueAuthGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<View
android:id="@+id/resetPasswordBreakerGradientContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.60"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_gradient_ftue_breaker" />
<Space
android:id="@+id/resetPasswordBreakerSpace1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerLogo"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside" />
<ImageView
android:id="@+id/resetPasswordBreakerLogo"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:importantForAccessibility="no"
android:src="@drawable/ic_email"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerSpace2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerSpace1" />
<Space
android:id="@+id/resetPasswordBreakerSpace2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerTitle"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerLogo" />
<TextView
android:id="@+id/resetPasswordBreakerTitle"
style="@style/Widget.Vector.TextView.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:transitionName="loginTitleTransition"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerSubtitle"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerSpace2"
tools:text="@string/ftue_auth_reset_password_breaker_title" />
<TextView
android:id="@+id/resetPasswordBreakerSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerSpace4"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerTitle"
tools:text="@string/ftue_auth_email_verification_subtitle" />
<Space
android:id="@+id/resetPasswordBreakerSpace4"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerResendEmail"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerSubtitle" />
<Button
android:id="@+id/resetPasswordBreakerFooter"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_set_email_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerResendEmail"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerSpace4" />
<Button
android:id="@+id/resetPasswordBreakerResendEmail"
style="@style/Widget.Vector.Button.Text.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/element_background_light"
android:text="@string/ftue_auth_email_resend_email"
android:textAllCaps="true"
android:textColor="?colorSecondary"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toTopOf="@id/resetPasswordBreakerSpace5"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerFooter" />
<Space
android:id="@+id/resetPasswordBreakerSpace5"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/resetPasswordBreakerResendEmail" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/emailEntryGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/emailEntryGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderIcon"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/emailEntryHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
android:src="@drawable/ic_email"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/emailEntryHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_email_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderIcon" />
<TextView
android:id="@+id/emailEntryHeaderSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/ftue_auth_reset_password_email_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/emailEntryInput"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderSubtitle" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/emailEntryInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/ftue_auth_email_entry_title"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:id="@+id/entrySpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/emailEntrySubmit"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/emailEntryInput"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="@+id/emailEntrySubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_set_email_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/newPasswordGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/newPasswordGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/newPasswordHeaderIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/newPasswordHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
android:src="@drawable/ic_new_password"
app:layout_constraintBottom_toTopOf="@id/newPasswordHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/newPasswordHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_new_password_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/newPasswordHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/newPasswordHeaderIcon" />
<TextView
android:id="@+id/newPasswordHeaderSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/ftue_auth_new_password_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/newPasswordHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/newPasswordInput"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/newPasswordHeaderSubtitle" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/newPasswordInput"
style="@style/Widget.Vector.TextInputLayout.Password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/ftue_auth_new_password_entry_title"
app:endIconMode="password_toggle"
app:layout_constraintBottom_toTopOf="@id/entrySignOutAll"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.Group
android:id="@+id/signedOutAllGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="entrySignOutAll,signOutAllLabel" />
<CheckBox
android:id="@+id/entrySignOutAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="-14dp"
android:buttonTint="@color/checkbox_tint_selector"
app:layout_constraintBottom_toTopOf="@id/newPasswordSubmit"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/newPasswordInput"
tools:ignore="NegativeMargin" />
<TextView
android:id="@+id/signOutAllLabel"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ftue_auth_sign_out_all_devices"
app:layout_constraintBottom_toTopOf="@id/entrySignOutAll"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/entrySignOutAll"
app:layout_constraintTop_toBottomOf="@id/entrySignOutAll" />
<Button
android:id="@+id/newPasswordSubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/ftue_auth_reset_password"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/newPasswordGutterEnd"
app:layout_constraintStart_toStartOf="@id/newPasswordGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySignOutAll" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -25,7 +25,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHeight_percent="0.60" app:layout_constraintHeight_percent="0.60"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_waiting_for_email_verification" /> tools:background="@drawable/bg_gradient_ftue_breaker" />
<Space <Space
android:id="@+id/emailVerificationSpace1" android:id="@+id/emailVerificationSpace1"

View file

@ -36,12 +36,21 @@
<string name="ftue_auth_email_title">Enter your email address</string> <string name="ftue_auth_email_title">Enter your email address</string>
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string> <string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
<string name="ftue_auth_email_entry_title">Email Address</string> <string name="ftue_auth_email_entry_title">Email Address</string>
<string name="ftue_auth_reset_password_email_subtitle">We will send you a verification link.</string>
<string name="ftue_auth_reset_password_breaker_title">Check your email.</string>
<string name="ftue_auth_new_password_entry_title">New Password</string>
<string name="ftue_auth_new_password_title">Choose a new password</string>
<string name="ftue_auth_new_password_subtitle">Make sure it\'s 8 characters or more.</string>
<string name="ftue_auth_reset_password">Reset password</string>
<string name="ftue_auth_sign_out_all_devices">Sign out all devices</string>
<string name="ftue_auth_email_verification_title">Check your email to verify.</string> <string name="ftue_auth_email_verification_title">Check your email to verify.</string>
<!-- Note for translators, %s is the users email address --> <!-- Note for translators, %s is the users email address -->
<string name="ftue_auth_email_verification_subtitle">To confirm your email address, tap the button in the email we just sent to %s</string> <string name="ftue_auth_email_verification_subtitle">To confirm your email address, tap the button in the email we just sent to %s</string>
<string name="ftue_auth_email_verification_footer">Did not receive an email?</string> <string name="ftue_auth_email_verification_footer">Did not receive an email?</string>
<string name="ftue_auth_email_resend_email">Resend email</string> <string name="ftue_auth_email_resend_email">Resend email</string>
<string name="ftue_auth_forgot_password">Forgot password</string>
<string name="ftue_auth_password_reset_confirmation">Password reset</string>
<string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string> <string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string>
</resources> </resources>

View file

@ -478,7 +478,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest { fun `given can successfully start password reset, when resetting password, then emits confirmation email sent`() = runTest {
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES)) viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
val test = viewModel.test() val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL) fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
@ -495,14 +495,35 @@ class OnboardingViewModelTest {
copy(isLoading = false, resetState = resetState) copy(isLoading = false, resetState = resetState)
} }
) )
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone) .assertEvents(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(AN_EMAIL))
.finish() .finish()
} }
@Test @Test
fun `given can successfully confirm reset password, when confirm reset password, then emits reset success`() = runTest { fun `given existing reset state, when resending reset password email, then triggers reset password and emits nothing`() = runTest {
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD))) viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
val test = viewModel.test() val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
viewModel.handle(OnboardingAction.ResendResetPassword)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertNoEvents()
.finish()
fakeLoginWizard.verifyResetPassword(AN_EMAIL)
}
@Test
fun `given combined login disabled, when confirming password reset, then opens reset password complete`() = runTest {
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginDisabled()
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD) fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard) fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
@ -514,7 +535,27 @@ class OnboardingViewModelTest {
{ copy(isLoading = true) }, { copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState()) } { copy(isLoading = false, resetState = ResetState()) }
) )
.assertEvents(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess) .assertEvents(OnboardingViewEvents.OpenResetPasswordComplete)
.finish()
}
@Test
fun `given combined login enabled, when confirming password reset, then emits reset password complete`() = runTest {
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginEnabled()
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
viewModel.handle(OnboardingAction.ResetPasswordMailConfirmed)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState()) }
)
.assertEvents(OnboardingViewEvents.OnResetPasswordComplete)
.finish() .finish()
} }

View file

@ -17,6 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import io.mockk.coJustRun import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
@ -29,4 +30,8 @@ class FakeLoginWizard : LoginWizard by mockk() {
fun givenConfirmResetPasswordSuccess(password: String) { fun givenConfirmResetPasswordSuccess(password: String) {
coJustRun { resetPasswordMailConfirmed(password) } coJustRun { resetPasswordMailConfirmed(password) }
} }
fun verifyResetPassword(email: String) {
coVerify { resetPassword(email) }
}
} }

View file

@ -30,4 +30,12 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
fun givenCombinedRegisterEnabled() { fun givenCombinedRegisterEnabled() {
every { isOnboardingCombinedRegisterEnabled() } returns true every { isOnboardingCombinedRegisterEnabled() } returns true
} }
fun givenCombinedLoginEnabled() {
every { isOnboardingCombinedLoginEnabled() } returns true
}
fun givenCombinedLoginDisabled() {
every { isOnboardingCombinedLoginEnabled() } returns false
}
} }