diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index c543f2ed3a..0ab0a8b64f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -31,8 +31,11 @@ interface RegistrationWizard { fun dummy(callback: MatrixCallback): Cancelable - fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable + fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable + fun validateEmail(callback: MatrixCallback): Cancelable + + val currentThreePid: String? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt index f0314b6c25..1160ee79d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -64,6 +64,7 @@ internal data class AuthParams( } +@JsonClass(generateAdapter = true) data class ThreePidCredentials( @Json(name = "client_secret") val clientSecret: String? = null, @@ -71,5 +72,6 @@ data class ThreePidCredentials( @Json(name = "id_server") val idServer: String? = null, + @Json(name = "sid") val sid: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 40feefa9f1..2be2687f44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -34,10 +34,17 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import okhttp3.OkHttpClient import java.util.* +// Container to store the data when a three pid is in validation step +internal data class ThreePidData( + val threePid: RegisterThreePid, + val registrationParams: RegistrationParams +) + /** * This class execute the registration request and is responsible to keep the session of interactive authentication */ @@ -56,6 +63,17 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: private val registerTask = DefaultRegisterTask(authAPI) private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) + private var currentThreePidData: ThreePidData? = null + + override val currentThreePid: String? + get() { + return when (val threePid = currentThreePidData?.threePid) { + is RegisterThreePid.Email -> threePid.email + is RegisterThreePid.Msisdn -> threePid.msisdn + null -> null + } + } + override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { return performRegistrationRequest(RegistrationParams(), callback) } @@ -98,29 +116,52 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable { - if (currentSession == null) { + override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } val job = GlobalScope.launch(coroutineDispatchers.main) { - val result = runCatching { + runCatching { registerAddThreePidTask.execute(RegisterAddThreePidTask.Params(threePid, clientSecret, sendAttempt++)) } - result.fold( - { - // TODO Do something with the data return by the hs? - callback.onSuccess(Unit) - }, - { - callback.onFailure(it) - } - ) + .fold( + { + // Store data + currentThreePidData = ThreePidData( + threePid, + RegistrationParams( + auth = AuthParams.createForEmailIdentity(safeSession, + ThreePidCredentials( + clientSecret = clientSecret, + sid = it.sid + ) + ) + )) + .also { threePidData -> + // and send the sid a first time + performRegistrationRequest(threePidData.registrationParams, callback) + } + }, + { + callback.onFailure(it) + } + ) } return CancelableCoroutine(job) } + override fun validateEmail(callback: MatrixCallback): Cancelable { + val safeParam = currentThreePidData?.registrationParams ?: run { + callback.onFailure(IllegalStateException("developer error, no pending three pid")) + return NoOpCancellable + } + + // Wait 10 seconds before doing the request + return performRegistrationRequest(safeParam, callback, 10_000) + } + override fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable { val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) @@ -150,28 +191,31 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { + private fun performRegistrationRequest(registrationParams: RegistrationParams, + callback: MatrixCallback, + delayMillis: Long = 0): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { - val result = runCatching { + runCatching { + if (delayMillis > 0) delay(delayMillis) registerTask.execute(RegisterTask.Params(registrationParams)) } - result.fold( - { - val sessionParams = SessionParams(it, homeServerConnectionConfig) - sessionParamsStore.save(sessionParams) - val session = sessionManager.getOrCreateSession(sessionParams) + .fold( + { + val sessionParams = SessionParams(it, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + val session = sessionManager.getOrCreateSession(sessionParams) - callback.onSuccess(RegistrationResult.Success(session)) - }, - { - if (it is Failure.RegistrationFlowError) { - currentSession = it.registrationFlowResponse.session - callback.onSuccess(RegistrationResult.FlowResponse(it.registrationFlowResponse.toFlowResult())) - } else { - callback.onFailure(it) - } - } - ) + callback.onSuccess(RegistrationResult.Success(session)) + }, + { + if (it is Failure.RegistrationFlowError) { + currentSession = it.registrationFlowResponse.session + callback.onSuccess(RegistrationResult.FlowResponse(it.registrationFlowResponse.toFlowResult())) + } else { + callback.onFailure(it) + } + } + ) } return CancelableCoroutine(job) } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d3fcc03a61..d3576c2625 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -165,6 +165,11 @@ interface FragmentModule { @FragmentKey(LoginGenericTextInputFormFragment::class) fun bindLoginGenericTextInputFormFragment(fragment: LoginGenericTextInputFormFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginWaitForEmailFragment::class) + fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment + @Binds @IntoMap @FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) 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 31ee596521..19119ecf0f 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 @@ -34,8 +34,10 @@ sealed class LoginAction : VectorViewModelAction { data class RegisterWith(val username: String, val password: String, val initialDeviceName: String) : RegisterAction() data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() - // TODO Confirm Email (from link in the email) + // TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX) data class ConfirmMsisdn(val code: String) : RegisterAction() + object ValidateEmail : RegisterAction() + data class CaptchaDone(val captchaResponse: String) : RegisterAction() object AcceptTerms : RegisterAction() object RegisterDummy : RegisterAction() 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 ef01e285b4..b80f20f511 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 @@ -90,6 +90,14 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java) } is LoginNavigation.OnResetPasswordSuccessDone -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + is LoginNavigation.OnSendEmailSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(it.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) + is LoginNavigation.OnSendMsisdnSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, it.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) } } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 1c839080c3..7ed1edc881 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -25,11 +25,14 @@ import butterknife.OnClick import com.airbnb.mvrx.args import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.auth.registration.RegisterThreePid +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection enum class TextInputFormFragmentMode { SetEmail, @@ -40,7 +43,8 @@ enum class TextInputFormFragmentMode { @Parcelize data class LoginGenericTextInputFormFragmentArgument( val mode: TextInputFormFragmentMode, - val mandatory: Boolean + val mandatory: Boolean, + val extra: String = "" ) : Parcelable /** @@ -88,7 +92,7 @@ class LoginGenericTextInputFormFragment @Inject constructor(private val errorFor } TextInputFormFragmentMode.ConfirmMsisdn -> { loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) - loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice) + loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra) loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER loginGenericTextInputFormOtherButton.isVisible = true @@ -141,7 +145,32 @@ class LoginGenericTextInputFormFragment @Inject constructor(private val errorFor } override fun onRegistrationError(throwable: Throwable) { - loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + if (throwable.is401()) { + // This is normal use case, we go to the mail waiting screen + loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")) + } else { + loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + } + } + TextInputFormFragmentMode.SetMsisdn -> { + if (throwable.is401()) { + // This is normal use case, we go to the enter code screen + loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")) + } else { + loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + } + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + // TODO + } + } + } + + private fun Throwable.is401(): Boolean { + return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && this.error.code == MatrixError.UNAUTHORIZED) } override fun resetViewModel() { 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 8eccaa0297..14ea19b4e0 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 @@ -27,6 +27,8 @@ sealed class LoginNavigation : VectorSharedAction { object OnForgetPasswordClicked : LoginNavigation() object OnResetPasswordSuccess : LoginNavigation() object OnResetPasswordSuccessDone : LoginNavigation() + data class OnSendEmailSuccess(val email: String) : LoginNavigation() + data class OnSendMsisdnSuccess(val msisdn: String) : 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/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 329ae84537..e22ed773ba 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 @@ -66,6 +66,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + val currentThreePid: String? + get() = registrationWizard?.currentThreePid + var isPasswordSent: Boolean = false private set @@ -108,9 +111,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.RegisterDummy -> handleRegisterDummy() is LoginAction.AddThreePid -> handleAddThreePid(action) is LoginAction.ConfirmMsisdn -> handleConfirmMsisdn(action) + is LoginAction.ValidateEmail -> handleValidateEmail() } } + private fun handleValidateEmail() { + // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state + currentTask?.cancel() + currentTask = registrationWizard?.validateEmail(registrationCallback) + } + private fun handleConfirmMsisdn(action: LoginAction.ConfirmMsisdn) { setState { copy(asyncRegistration = Loading()) } currentTask = registrationWizard?.confirmMsisdn(action.code, registrationCallback) @@ -149,11 +159,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleAddThreePid(action: LoginAction.AddThreePid) { - // TODO Use the same async? setState { copy(asyncRegistration = Loading()) } - currentTask = registrationWizard?.addThreePid(action.threePid, object : MatrixCallback { - override fun onSuccess(data: Unit) { - // TODO Notify the View + currentTask = registrationWizard?.addThreePid(action.threePid, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { setState { copy( asyncRegistration = Uninitialized diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt new file mode 100644 index 0000000000..5b9ddd240a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt @@ -0,0 +1,80 @@ +/* + * 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.os.Parcelable +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.args +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_wait_for_email.* +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@Parcelize +data class LoginWaitForEmailFragmentArgument( + val email: String +) : Parcelable + +/** + * In this screen, the user is asked to check his emails + */ +class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() { + + private val params: LoginWaitForEmailFragmentArgument by args() + + override fun getLayoutResId() = R.layout.fragment_login_wait_for_email + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + + loginViewModel.handle(LoginAction.ValidateEmail) + } + + private fun setupUi() { + loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email) + } + + override fun onRegistrationError(throwable: Throwable) { + if (throwable.is401()) { + // Try again, with a delay + loginViewModel.handle(LoginAction.ValidateEmail) + } else { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } + } + + private fun Throwable.is401(): Boolean { + return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && this.error.code == MatrixError.UNAUTHORIZED) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetLogin) + } +} diff --git a/vector/src/main/res/layout/fragment_login_wait_for_email.xml b/vector/src/main/res/layout/fragment_login_wait_for_email.xml new file mode 100644 index 0000000000..dee43d9359 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_wait_for_email.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 07a75419b6..8d193dbfe5 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -108,4 +108,7 @@ Please perform the captcha challenge Accept terms to continue + Please check your email + We just sent an email to %1$s.\nPlease click on the link it contains to continue the account creation. +