diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index c1dfa465fb..def8293798 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -68,4 +68,9 @@ interface Authenticator { * Create a session after a SSO successful login */ fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + + /** + * Reset user password + */ + fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index ff49d4308b..995ec0aedb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -131,6 +131,25 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } + override fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + resetPasswordInternal(/*homeServerConnectionConfig, email, newPassword*/) + } + result.foldToCallback(callback) + } + return CancelableCoroutine(job) + } + + private fun resetPasswordInternal(/*homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String*/) { + // TODO + error("Not implemented") + //val authAPI = buildAuthAPI(homeServerConnectionConfig) + //executeRequest { + // apiCall = authAPI.getLoginFlows() + //} + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 754372e82c..a03af1376d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -26,6 +26,7 @@ sealed class LoginAction : VectorViewModelAction { data class Login(val login: String, val password: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() + data class ResetPassword(val email: String, val newPassword: String) : LoginAction() // Reset actions open class ResetAction : LoginAction() @@ -34,4 +35,5 @@ sealed class LoginAction : VectorViewModelAction { object ResetHomeServerUrl : ResetAction() object ResetSignMode : ResetAction() object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c3a748f442..7bc713a4f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -65,11 +65,17 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() - is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() + is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OnForgetPasswordClicked -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordFragment::class.java) + is LoginNavigation.OnResetPasswordSuccess -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java) + } + is LoginNavigation.OnResetPasswordSuccessDone -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 58017d0f21..14465fa48b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View import androidx.core.view.isVisible +import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -50,7 +51,7 @@ class LoginFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupUi() - setupLoginButton() + setupSubmitButton() setupPasswordReveal() } @@ -74,19 +75,17 @@ class LoginFragment @Inject constructor( loginServerIcon.setImageResource(R.drawable.ic_logo_modular) // TODO loginTitle.text = getString(R.string.login_connect_to, "TODO") - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } } } - private fun setupLoginButton() { + private fun setupSubmitButton() { Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, @@ -105,6 +104,11 @@ class LoginFragment @Inject constructor( loginSubmit.setOnClickListener { authenticate() } } + @OnClick(R.id.forgetPasswordButton) + fun forgetPasswordClicked() { + loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + } + private fun setupPasswordReveal() { passwordShown = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index f0ff456734..8eccaa0297 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -24,5 +24,9 @@ sealed class LoginNavigation : VectorSharedAction { object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() object OnSignModeSelected : LoginNavigation() + object OnForgetPasswordClicked : LoginNavigation() + object OnResetPasswordSuccess : LoginNavigation() + object OnResetPasswordSuccessDone : LoginNavigation() + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt new file mode 100644 index 0000000000..600013c5ba --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.showPassword +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.subscribeBy +import kotlinx.android.synthetic.main.fragment_login.* +import kotlinx.android.synthetic.main.fragment_login.passwordField +import kotlinx.android.synthetic.main.fragment_login.passwordFieldTil +import kotlinx.android.synthetic.main.fragment_login.passwordReveal +import kotlinx.android.synthetic.main.fragment_login_reset_password.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { + + private var passwordShown = false + + override fun getLayoutResId() = R.layout.fragment_login_reset_password + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupSubmitButton() + setupPasswordReveal() + } + + private fun setupUi() { + resetPasswordTitle.text = getString(R.string.login_reset_password_on, loginViewModel.getHomeServerUrlSimple()) + } + + private fun setupSubmitButton() { + Observable + .combineLatest( + resetPasswordEmail.textChanges().map { it.trim().isNotEmpty() }, + passwordField.textChanges().map { it.trim().isNotEmpty() }, + BiFunction { isEmailNotEmpty, isPasswordNotEmpty -> + isEmailNotEmpty && isPasswordNotEmpty + } + ) + .subscribeBy { + resetPasswordEmail.error = null + passwordFieldTil.error = null + loginSubmit.isEnabled = it + } + .disposeOnDestroy() + + resetPasswordSubmit.setOnClickListener { submit() } + } + + private fun submit() { + val email = resetPasswordEmail.text?.trim().toString() + val password = passwordField.text?.trim().toString() + + // TODO Add static check? + + loginViewModel.handle(LoginAction.ResetPassword(email, password)) + } + + private fun setupPasswordReveal() { + passwordShown = false + + passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + passwordField.showPassword(passwordShown) + + if (passwordShown) { + passwordReveal.setImageResource(R.drawable.ic_eye_closed_black) + passwordReveal.contentDescription = getString(R.string.a11y_hide_password) + } else { + passwordReveal.setImageResource(R.drawable.ic_eye_black) + passwordReveal.contentDescription = getString(R.string.a11y_show_password) + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } + + override fun invalidate() = withState(loginViewModel) { state -> + when (state.asyncResetPassword) { + is Loading -> { + // Ensure new password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + // TODO This does not work, we want the error to be on without text. Fix that + resetPasswordEmailTil.error = "" + // TODO Handle error text properly + passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) + } + is Success -> { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccess) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt new file mode 100644 index 0000000000..20e209573e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import butterknife.OnClick +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_reset_password_success.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_reset_password_success + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + private fun setupUi() { + resetPasswordSuccessNotice.text = getString(R.string.login_reset_password_success_notice, loginViewModel.resetPasswordEmail) + } + + @OnClick(R.id.resetPasswordSuccessSubmit) + fun submit() { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccessDone) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 9ba5fa739b..ac8362eb31 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -57,9 +57,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi var serverType: ServerType = ServerType.MatrixOrg private set - var signMode: SignMode = SignMode.Unknown private set + var resetPasswordEmail: String? = null + private set private var loginConfig: LoginConfig? = null @@ -74,6 +75,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetAction -> handleResetAction(action) } } @@ -109,6 +111,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } } + LoginAction.ResetResetPassword -> { + resetPasswordEmail = null + setState { + copy( + asyncResetPassword = Uninitialized + ) + } + } } } @@ -124,6 +134,45 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi loginConfig = action.loginConfig } + private fun handleResetPassword(action: LoginAction.ResetPassword) { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncResetPassword = Fail(Throwable("Bad configuration")) + ) + } + } else { + resetPasswordEmail = action.email + + setState { + copy( + asyncResetPassword = Loading() + ) + } + + currentTask = authenticator.resetPassword(homeServerConnectionConfigFinal, action.email, action.newPassword, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + asyncResetPassword = Success(data) + ) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncResetPassword = Fail(failure) + ) + } + } + }) + } + } + private fun handleLogin(action: LoginAction.Login) { val homeServerConnectionConfigFinal = homeServerConnectionConfig @@ -259,4 +308,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi fun getHomeServerUrl(): String { return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } + + fun getHomeServerUrlSimple(): String { + return getHomeServerUrl().substringAfter("://") + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0853765d63..5ecfd7fad6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,17 +16,21 @@ package im.vector.riotx.features.login + import com.airbnb.mvrx.* data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized, + val asyncResetPassword: Async = Uninitialized ) : MvRxState { + fun isLoading(): Boolean { // TODO Add other async here return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading + || asyncResetPassword is Loading } fun isUserLogged(): Boolean { diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml new file mode 100644 index 0000000000..8662b5b077 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml new file mode 100644 index 0000000000..b777250f8d --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 17bb99ac4a..089ac19b7e 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -38,9 +38,11 @@ Custom & advanced settings Continue + Connect to %1$s Connect to Modular Connect to a custom server + Sign in to %1$s Sign Up Sign In @@ -55,4 +57,16 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + + Reset password on %1$s + A verification email will be sent to your inbox to confirm setting your new password. + Next + Email + New password + Check your inbox + + A verification email was sent to %1$s. + Tap on the link to confirm your new password. + Back to Sign In +