mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
Login screens: Wait for email validation screen
This commit is contained in:
parent
1f161b7e23
commit
b8a3ad0c43
12 changed files with 266 additions and 39 deletions
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
41
vector/src/main/res/layout/fragment_login_wait_for_email.xml
Normal file
41
vector/src/main/res/layout/fragment_login_wait_for_email.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue