mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 18:36:21 +03:00
Merge pull request #6263 from vector-im/feature/adm/ftue-forgot-password
[FTUE] Forgot password
This commit is contained in:
commit
72c4af0026
26 changed files with 856 additions and 95 deletions
1
changelog.d/5284.wip
Normal file
1
changelog.d/5284.wip
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Adds support for resetting the password during the FTUE onboarding journey
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
10
vector/src/main/res/drawable/ic_new_password.xml
Normal file
10
vector/src/main/res/drawable/ic_new_password.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue