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.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)
}
})
}

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

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

View File

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

View File

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

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() {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone))
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordComplete))
}
override fun resetViewModel() {

View File

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

View File

@ -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
}

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_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 @@
</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
android:id="@+id/actionSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginSubmit"
app:layout_constraintHeight_percent="0.02"
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
app:layout_constraintTop_toBottomOf="@id/loginForgotPassword" />
<Button
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"
app:layout_constraintHeight_percent="0.60"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_waiting_for_email_verification" />
tools:background="@drawable/bg_gradient_ftue_breaker" />
<Space
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_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_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>
<!-- 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_footer">Did not receive an 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>
</resources>

View File

@ -478,7 +478,7 @@ class OnboardingViewModelTest {
}
@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))
val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
@ -495,14 +495,35 @@ class OnboardingViewModelTest {
copy(isLoading = false, resetState = resetState)
}
)
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
.assertEvents(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(AN_EMAIL))
.finish()
}
@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)))
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)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
@ -514,7 +535,27 @@ class OnboardingViewModelTest {
{ copy(isLoading = true) },
{ 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()
}

View File

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

View File

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