Login screens: Wait for email validation screen

This commit is contained in:
Benoit Marty 2019-11-20 17:23:41 +01:00
parent 1f161b7e23
commit b8a3ad0c43
12 changed files with 266 additions and 39 deletions

View file

@ -31,8 +31,11 @@ interface RegistrationWizard {
fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable
fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<Unit>): Cancelable
fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable
fun confirmMsisdn(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun validateEmail(callback: MatrixCallback<RegistrationResult>): Cancelable
val currentThreePid: String?
}

View file

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

View file

@ -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<RegistrationResult>): Cancelable {
return performRegistrationRequest(RegistrationParams(), callback)
}
@ -98,29 +116,52 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig:
), callback)
}
override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<Unit>): Cancelable {
if (currentSession == null) {
override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): 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<RegistrationResult>): 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<RegistrationResult>): 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<RegistrationResult>): Cancelable {
private fun performRegistrationRequest(registrationParams: RegistrationParams,
callback: MatrixCallback<RegistrationResult>,
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Unit> {
override fun onSuccess(data: Unit) {
// TODO Notify the View
currentTask = registrationWizard?.addThreePid(action.threePid, object : MatrixCallback<RegistrationResult> {
override fun onSuccess(data: RegistrationResult) {
setState {
copy(
asyncRegistration = Uninitialized

View file

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

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView style="@style/LoginLogo" />
<LinearLayout
style="@style/LoginFormContainer"
android:orientation="vertical">
<TextView
android:id="@+id/loginWaitForEmailTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_wait_for_email_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginWaitForEmailNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="@string/login_wait_for_email_notice" />
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:indeterminate="true" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -108,4 +108,7 @@
<string name="login_a11y_captcha_container">Please perform the captcha challenge</string>
<string name="login_terms_title">Accept terms to continue</string>
<string name="login_wait_for_email_title">Please check your email</string>
<string name="login_wait_for_email_notice">We just sent an email to %1$s.\nPlease click on the link it contains to continue the account creation.</string>
</resources>