Merge pull request #6263 from vector-im/feature/adm/ftue-forgot-password
[FTUE] Forgot password
This commit is contained in:
commit
72c4af0026
|
@ -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.text.Editable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import im.vector.app.core.platform.SimpleTextWatcher
|
import im.vector.app.core.platform.SimpleTextWatcher
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.widget.textChanges
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
|
|
||||||
fun TextInputLayout.editText() = this.editText!!
|
fun TextInputLayout.editText() = this.editText!!
|
||||||
|
@ -37,11 +41,18 @@ fun TextInputLayout.content() = editText().text.toString()
|
||||||
|
|
||||||
fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty()
|
fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty()
|
||||||
|
|
||||||
fun TextInputLayout.associateContentStateWith(button: View) {
|
fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) {
|
||||||
|
editText().textChanges()
|
||||||
|
.onEach { error = null }
|
||||||
|
.launchIn(lifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.associateContentStateWith(button: View, enabledPredicate: (String) -> Boolean = { it.isNotEmpty() }) {
|
||||||
|
button.isEnabled = enabledPredicate(content())
|
||||||
editText().addTextChangedListener(object : SimpleTextWatcher() {
|
editText().addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
override fun afterTextChanged(s: Editable) {
|
override fun afterTextChanged(s: Editable) {
|
||||||
val newContent = s.toString()
|
val newContent = s.toString()
|
||||||
button.isEnabled = newContent.isNotEmpty()
|
button.isEnabled = enabledPredicate(newContent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,9 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||||
data class LoginWithToken(val loginToken: String) : OnboardingAction
|
data class LoginWithToken(val loginToken: String) : OnboardingAction
|
||||||
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
|
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
|
||||||
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
|
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
|
||||||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
data class ResetPassword(val email: String, val newPassword: String?) : OnboardingAction
|
||||||
|
data class ConfirmNewPassword(val newPassword: String, val signOutAllDevices: Boolean) : OnboardingAction
|
||||||
|
object ResendResetPassword : OnboardingAction
|
||||||
object ResetPasswordMailConfirmed : OnboardingAction
|
object ResetPasswordMailConfirmed : OnboardingAction
|
||||||
|
|
||||||
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction
|
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction
|
||||||
|
|
|
@ -47,9 +47,11 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
||||||
object OnHomeserverEdited : OnboardingViewEvents()
|
object OnHomeserverEdited : OnboardingViewEvents()
|
||||||
data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents()
|
data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents()
|
||||||
object OnForgetPasswordClicked : OnboardingViewEvents()
|
object OnForgetPasswordClicked : OnboardingViewEvents()
|
||||||
object OnResetPasswordSendThreePidDone : OnboardingViewEvents()
|
|
||||||
object OnResetPasswordMailConfirmationSuccess : OnboardingViewEvents()
|
data class OnResetPasswordEmailConfirmationSent(val email: String) : OnboardingViewEvents()
|
||||||
object OnResetPasswordMailConfirmationSuccessDone : OnboardingViewEvents()
|
object OpenResetPasswordComplete : OnboardingViewEvents()
|
||||||
|
object OnResetPasswordBreakerConfirmed : OnboardingViewEvents()
|
||||||
|
object OnResetPasswordComplete : OnboardingViewEvents()
|
||||||
|
|
||||||
data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents()
|
data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents()
|
||||||
data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()
|
data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()
|
||||||
|
|
|
@ -149,6 +149,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||||
|
OnboardingAction.ResendResetPassword -> handleResendResetPassword()
|
||||||
|
is OnboardingAction.ConfirmNewPassword -> handleResetPasswordConfirmed(action)
|
||||||
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||||
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
|
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
|
||||||
is OnboardingAction.ResetAction -> handleResetAction(action)
|
is OnboardingAction.ResetAction -> handleResetAction(action)
|
||||||
|
@ -439,25 +441,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleResetPassword(action: OnboardingAction.ResetPassword) {
|
private fun handleResetPassword(action: OnboardingAction.ResetPassword) {
|
||||||
val safeLoginWizard = loginWizard
|
startResetPasswordFlow(action.email) {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = false, resetState = createResetState(action, selectedHomeserver)) }
|
||||||
currentJob = viewModelScope.launch {
|
_viewEvents.post(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(action.email))
|
||||||
runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
|
|
||||||
onSuccess = {
|
|
||||||
val state = awaitState()
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
isLoading = false,
|
|
||||||
resetState = createResetState(action, state.selectedHomeserver)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
setState { copy(isLoading = false) }
|
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,6 +453,41 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported
|
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun handleResendResetPassword() {
|
||||||
|
withState { state ->
|
||||||
|
val resetState = state.resetState
|
||||||
|
when (resetState.email) {
|
||||||
|
null -> _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No reset email has been set")))
|
||||||
|
else -> {
|
||||||
|
startResetPasswordFlow(resetState.email) {
|
||||||
|
setState { copy(isLoading = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startResetPasswordFlow(email: String, onSuccess: suspend () -> Unit) {
|
||||||
|
val safeLoginWizard = loginWizard
|
||||||
|
setState { copy(isLoading = true) }
|
||||||
|
currentJob = viewModelScope.launch {
|
||||||
|
runCatching { safeLoginWizard.resetPassword(email) }.fold(
|
||||||
|
onSuccess = { onSuccess.invoke() },
|
||||||
|
onFailure = {
|
||||||
|
setState { copy(isLoading = false) }
|
||||||
|
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResetPasswordConfirmed(action: OnboardingAction.ConfirmNewPassword) {
|
||||||
|
setState { copy(isLoading = true) }
|
||||||
|
currentJob = viewModelScope.launch {
|
||||||
|
confirmPasswordReset(action.newPassword, action.signOutAllDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleResetPasswordMailConfirmed() {
|
private fun handleResetPasswordMailConfirmed() {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
|
@ -476,27 +497,28 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
setState { copy(isLoading = false) }
|
setState { copy(isLoading = false) }
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set")))
|
_viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set")))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> confirmPasswordReset(newPassword, logoutAllDevices = true)
|
||||||
runCatching { loginWizard.resetPasswordMailConfirmed(newPassword) }.fold(
|
|
||||||
onSuccess = {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
isLoading = false,
|
|
||||||
resetState = ResetState()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_viewEvents.post(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
setState { copy(isLoading = false) }
|
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun confirmPasswordReset(newPassword: String, logoutAllDevices: Boolean) {
|
||||||
|
runCatching { loginWizard.resetPasswordMailConfirmed(newPassword, logoutAllDevices = logoutAllDevices) }.fold(
|
||||||
|
onSuccess = {
|
||||||
|
setState { copy(isLoading = false, resetState = ResetState()) }
|
||||||
|
val nextEvent = when {
|
||||||
|
vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete
|
||||||
|
else -> OnboardingViewEvents.OpenResetPasswordComplete
|
||||||
|
}
|
||||||
|
_viewEvents.post(nextEvent)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
setState { copy(isLoading = false) }
|
||||||
|
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
|
|
|
@ -61,6 +61,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||||
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
|
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
|
||||||
|
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
|
|
|
@ -21,8 +21,8 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
|
@ -34,10 +34,7 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||||
import reactivecircus.flowbinding.android.widget.textChanges
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
|
class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
|
||||||
|
@ -66,9 +63,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
|
||||||
}
|
}
|
||||||
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
|
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
|
||||||
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
|
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
|
||||||
views.chooseServerInput.editText().textChanges()
|
views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
.onEach { views.chooseServerInput.error = null }
|
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateServerUrl() {
|
private fun updateServerUrl() {
|
||||||
|
|
|
@ -20,19 +20,15 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import im.vector.app.core.extensions.associateContentStateWith
|
import im.vector.app.core.extensions.associateContentStateWith
|
||||||
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
|
||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
import im.vector.app.databinding.FragmentFtueEmailInputBinding
|
import im.vector.app.databinding.FragmentFtueEmailInputBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.onboarding.RegisterAction
|
import im.vector.app.features.onboarding.RegisterAction
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
import reactivecircus.flowbinding.android.widget.textChanges
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueEmailInputBinding>() {
|
class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueEmailInputBinding>() {
|
||||||
|
@ -47,16 +43,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit)
|
views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() })
|
||||||
views.emailEntryInput.setOnImeDoneListener { updateEmail() }
|
views.emailEntryInput.setOnImeDoneListener { updateEmail() }
|
||||||
|
views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
views.emailEntrySubmit.debouncedClicks { updateEmail() }
|
views.emailEntrySubmit.debouncedClicks { updateEmail() }
|
||||||
|
|
||||||
views.emailEntryInput.editText().textChanges()
|
|
||||||
.onEach {
|
|
||||||
views.emailEntryInput.error = null
|
|
||||||
views.emailEntrySubmit.isEnabled = it.isEmail()
|
|
||||||
}
|
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEmail() {
|
private fun updateEmail() {
|
||||||
|
|
|
@ -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() {
|
private fun submit() {
|
||||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone))
|
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordComplete))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resetViewModel() {
|
override fun resetViewModel() {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentTransaction
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
|
||||||
import im.vector.app.core.extensions.addFragment
|
import im.vector.app.core.extensions.addFragment
|
||||||
import im.vector.app.core.extensions.addFragmentToBackstack
|
import im.vector.app.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.app.core.extensions.popBackstack
|
import im.vector.app.core.extensions.popBackstack
|
||||||
|
@ -162,30 +162,38 @@ class FtueAuthVariant(
|
||||||
)
|
)
|
||||||
is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents)
|
is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents)
|
||||||
is OnboardingViewEvents.OnForgetPasswordClicked ->
|
is OnboardingViewEvents.OnForgetPasswordClicked ->
|
||||||
|
when {
|
||||||
|
vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordEmailEntryFragment::class.java)
|
||||||
|
else -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordFragment::class.java)
|
||||||
|
}
|
||||||
|
is OnboardingViewEvents.OnResetPasswordEmailConfirmationSent -> {
|
||||||
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
when {
|
||||||
|
vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack(
|
||||||
|
FtueAuthResetPasswordBreakerFragment::class.java,
|
||||||
|
FtueAuthResetPasswordBreakerArgument(viewEvents.email),
|
||||||
|
)
|
||||||
|
else -> activity.addFragmentToBackstack(
|
||||||
|
views.loginFragmentContainer,
|
||||||
|
FtueAuthResetPasswordMailConfirmationFragment::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnboardingViewEvents.OnResetPasswordBreakerConfirmed -> {
|
||||||
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
activity.addFragmentToBackstack(
|
activity.addFragmentToBackstack(
|
||||||
views.loginFragmentContainer,
|
views.loginFragmentContainer,
|
||||||
FtueAuthResetPasswordFragment::class.java,
|
FtueAuthResetPasswordEntryFragment::class.java,
|
||||||
option = commonOption
|
|
||||||
)
|
|
||||||
is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> {
|
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
|
||||||
activity.addFragmentToBackstack(
|
|
||||||
views.loginFragmentContainer,
|
|
||||||
FtueAuthResetPasswordMailConfirmationFragment::class.java,
|
|
||||||
option = commonOption
|
option = commonOption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> {
|
is OnboardingViewEvents.OpenResetPasswordComplete -> {
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
activity.addFragmentToBackstack(
|
addLoginStageFragmentToBackstack(FtueAuthResetPasswordSuccessFragment::class.java)
|
||||||
views.loginFragmentContainer,
|
|
||||||
FtueAuthResetPasswordSuccessFragment::class.java,
|
|
||||||
option = commonOption
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> {
|
OnboardingViewEvents.OnResetPasswordComplete -> {
|
||||||
// Go back to the login fragment
|
Toast.makeText(activity, R.string.ftue_auth_password_reset_confirmation, Toast.LENGTH_SHORT).show()
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
activity.popBackstack()
|
||||||
}
|
}
|
||||||
is OnboardingViewEvents.OnSendEmailSuccess -> {
|
is OnboardingViewEvents.OnSendEmailSuccess -> {
|
||||||
openWaitForEmailVerification(viewEvents.email)
|
openWaitForEmailVerification(viewEvents.email)
|
||||||
|
@ -496,4 +504,14 @@ class FtueAuthVariant(
|
||||||
option = commonOption
|
option = commonOption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addLoginStageFragmentToBackstack(fragmentClass: Class<out Fragment>, params: Parcelable? = null) {
|
||||||
|
activity.addFragmentToBackstack(
|
||||||
|
views.loginFragmentContainer,
|
||||||
|
fragmentClass,
|
||||||
|
params,
|
||||||
|
tag = FRAGMENT_LOGIN_TAG,
|
||||||
|
option = commonOption
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,12 +58,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
private fun setupUi() {
|
||||||
views.emailVerificationGradientContainer.setBackgroundResource(
|
views.emailVerificationGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground())
|
||||||
when (themeProvider.isLightTheme()) {
|
|
||||||
true -> R.drawable.bg_waiting_for_email_verification
|
|
||||||
false -> R.drawable.bg_color_background
|
|
||||||
}
|
|
||||||
)
|
|
||||||
views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title)
|
views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title)
|
||||||
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
|
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
|
||||||
views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email)
|
views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email)
|
||||||
|
|
|
@ -18,9 +18,11 @@ package im.vector.app.features.onboarding.ftueauth
|
||||||
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.hasContentFlow
|
import im.vector.app.core.extensions.hasContentFlow
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.themes.ThemeProvider
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
@ -49,3 +51,8 @@ fun observeContentChangesAndResetErrors(username: TextInputLayout, password: Tex
|
||||||
submit.isEnabled = it
|
submit.isEnabled = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) {
|
||||||
|
true -> R.drawable.bg_gradient_ftue_breaker
|
||||||
|
false -> R.drawable.bg_color_background
|
||||||
|
}
|
||||||
|
|
|
@ -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_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/login_signup_password_hint"
|
android:hint="@string/login_signup_password_hint"
|
||||||
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
|
app:layout_constraintBottom_toTopOf="@id/loginForgotPassword"
|
||||||
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/entrySpacing">
|
app:layout_constraintTop_toBottomOf="@id/entrySpacing">
|
||||||
|
@ -184,13 +184,27 @@
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/loginForgotPassword"
|
||||||
|
style="@style/Widget.Vector.Button.Text.Login"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ftue_auth_forgot_password"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="?colorSecondary"
|
||||||
|
app:layout_constraintHorizontal_bias="1"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/actionSpacing"
|
android:id="@+id/actionSpacing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/loginSubmit"
|
app:layout_constraintBottom_toTopOf="@id/loginSubmit"
|
||||||
app:layout_constraintHeight_percent="0.02"
|
app:layout_constraintHeight_percent="0.02"
|
||||||
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
|
app:layout_constraintTop_toBottomOf="@id/loginForgotPassword" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/loginSubmit"
|
android:id="@+id/loginSubmit"
|
||||||
|
|
|
@ -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"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintHeight_percent="0.60"
|
app:layout_constraintHeight_percent="0.60"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:background="@drawable/bg_waiting_for_email_verification" />
|
tools:background="@drawable/bg_gradient_ftue_breaker" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/emailVerificationSpace1"
|
android:id="@+id/emailVerificationSpace1"
|
||||||
|
|
|
@ -36,12 +36,21 @@
|
||||||
<string name="ftue_auth_email_title">Enter your email address</string>
|
<string name="ftue_auth_email_title">Enter your email address</string>
|
||||||
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
|
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
|
||||||
<string name="ftue_auth_email_entry_title">Email Address</string>
|
<string name="ftue_auth_email_entry_title">Email Address</string>
|
||||||
|
<string name="ftue_auth_reset_password_email_subtitle">We will send you a verification link.</string>
|
||||||
|
<string name="ftue_auth_reset_password_breaker_title">Check your email.</string>
|
||||||
|
<string name="ftue_auth_new_password_entry_title">New Password</string>
|
||||||
|
<string name="ftue_auth_new_password_title">Choose a new password</string>
|
||||||
|
<string name="ftue_auth_new_password_subtitle">Make sure it\'s 8 characters or more.</string>
|
||||||
|
<string name="ftue_auth_reset_password">Reset password</string>
|
||||||
|
<string name="ftue_auth_sign_out_all_devices">Sign out all devices</string>
|
||||||
|
|
||||||
<string name="ftue_auth_email_verification_title">Check your email to verify.</string>
|
<string name="ftue_auth_email_verification_title">Check your email to verify.</string>
|
||||||
<!-- Note for translators, %s is the users email address -->
|
<!-- Note for translators, %s is the users email address -->
|
||||||
<string name="ftue_auth_email_verification_subtitle">To confirm your email address, tap the button in the email we just sent to %s</string>
|
<string name="ftue_auth_email_verification_subtitle">To confirm your email address, tap the button in the email we just sent to %s</string>
|
||||||
<string name="ftue_auth_email_verification_footer">Did not receive an email?</string>
|
<string name="ftue_auth_email_verification_footer">Did not receive an email?</string>
|
||||||
<string name="ftue_auth_email_resend_email">Resend email</string>
|
<string name="ftue_auth_email_resend_email">Resend email</string>
|
||||||
|
<string name="ftue_auth_forgot_password">Forgot password</string>
|
||||||
|
<string name="ftue_auth_password_reset_confirmation">Password reset</string>
|
||||||
|
|
||||||
<string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string>
|
<string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -478,7 +478,7 @@ class OnboardingViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest {
|
fun `given can successfully start password reset, when resetting password, then emits confirmation email sent`() = runTest {
|
||||||
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
|
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
|
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
|
||||||
|
@ -495,14 +495,35 @@ class OnboardingViewModelTest {
|
||||||
copy(isLoading = false, resetState = resetState)
|
copy(isLoading = false, resetState = resetState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
|
.assertEvents(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(AN_EMAIL))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given can successfully confirm reset password, when confirm reset password, then emits reset success`() = runTest {
|
fun `given existing reset state, when resending reset password email, then triggers reset password and emits nothing`() = runTest {
|
||||||
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
|
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
|
||||||
|
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.ResendResetPassword)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStatesChanges(
|
||||||
|
initialState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertNoEvents()
|
||||||
|
.finish()
|
||||||
|
fakeLoginWizard.verifyResetPassword(AN_EMAIL)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given combined login disabled, when confirming password reset, then opens reset password complete`() = runTest {
|
||||||
|
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
|
||||||
|
val test = viewModel.test()
|
||||||
|
fakeVectorFeatures.givenCombinedLoginDisabled()
|
||||||
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
|
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
|
||||||
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
|
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
|
||||||
|
|
||||||
|
@ -514,7 +535,27 @@ class OnboardingViewModelTest {
|
||||||
{ copy(isLoading = true) },
|
{ copy(isLoading = true) },
|
||||||
{ copy(isLoading = false, resetState = ResetState()) }
|
{ copy(isLoading = false, resetState = ResetState()) }
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess)
|
.assertEvents(OnboardingViewEvents.OpenResetPasswordComplete)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given combined login enabled, when confirming password reset, then emits reset password complete`() = runTest {
|
||||||
|
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
|
||||||
|
val test = viewModel.test()
|
||||||
|
fakeVectorFeatures.givenCombinedLoginEnabled()
|
||||||
|
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
|
||||||
|
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.ResetPasswordMailConfirmed)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStatesChanges(
|
||||||
|
initialState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false, resetState = ResetState()) }
|
||||||
|
)
|
||||||
|
.assertEvents(OnboardingViewEvents.OnResetPasswordComplete)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
import io.mockk.coJustRun
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||||
|
|
||||||
|
@ -29,4 +30,8 @@ class FakeLoginWizard : LoginWizard by mockk() {
|
||||||
fun givenConfirmResetPasswordSuccess(password: String) {
|
fun givenConfirmResetPasswordSuccess(password: String) {
|
||||||
coJustRun { resetPasswordMailConfirmed(password) }
|
coJustRun { resetPasswordMailConfirmed(password) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun verifyResetPassword(email: String) {
|
||||||
|
coVerify { resetPassword(email) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,12 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
|
||||||
fun givenCombinedRegisterEnabled() {
|
fun givenCombinedRegisterEnabled() {
|
||||||
every { isOnboardingCombinedRegisterEnabled() } returns true
|
every { isOnboardingCombinedRegisterEnabled() } returns true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCombinedLoginEnabled() {
|
||||||
|
every { isOnboardingCombinedLoginEnabled() } returns true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenCombinedLoginDisabled() {
|
||||||
|
every { isOnboardingCombinedLoginEnabled() } returns false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue