mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-21 17:05:39 +03:00
removing login2 classes
This commit is contained in:
parent
62f7b40a3e
commit
19261ab2d0
37 changed files with 12 additions and 4598 deletions
|
@ -18,6 +18,5 @@ package im.vector.app.config
|
|||
|
||||
enum class OnboardingVariant {
|
||||
LEGACY,
|
||||
LOGIN_2,
|
||||
FTUE_AUTH
|
||||
}
|
||||
|
|
|
@ -79,24 +79,6 @@ import im.vector.app.features.login.LoginSplashFragment
|
|||
import im.vector.app.features.login.LoginWaitForEmailFragment
|
||||
import im.vector.app.features.login.LoginWebFragment
|
||||
import im.vector.app.features.login.terms.LoginTermsFragment
|
||||
import im.vector.app.features.login2.LoginCaptchaFragment2
|
||||
import im.vector.app.features.login2.LoginFragmentSigninPassword2
|
||||
import im.vector.app.features.login2.LoginFragmentSigninUsername2
|
||||
import im.vector.app.features.login2.LoginFragmentSignupPassword2
|
||||
import im.vector.app.features.login2.LoginFragmentSignupUsername2
|
||||
import im.vector.app.features.login2.LoginFragmentToAny2
|
||||
import im.vector.app.features.login2.LoginGenericTextInputFormFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2
|
||||
import im.vector.app.features.login2.LoginServerSelectionFragment2
|
||||
import im.vector.app.features.login2.LoginServerUrlFormFragment2
|
||||
import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2
|
||||
import im.vector.app.features.login2.LoginSsoOnlyFragment2
|
||||
import im.vector.app.features.login2.LoginWaitForEmailFragment2
|
||||
import im.vector.app.features.login2.LoginWebFragment2
|
||||
import im.vector.app.features.login2.created.AccountCreatedFragment
|
||||
import im.vector.app.features.login2.terms.LoginTermsFragment2
|
||||
import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
|
||||
import im.vector.app.features.matrixto.MatrixToUserFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
|
||||
|
@ -334,96 +316,6 @@ interface FragmentModule {
|
|||
@FragmentKey(LoginWaitForEmailFragment::class)
|
||||
fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginFragmentSigninUsername2::class)
|
||||
fun bindLoginFragmentSigninUsername2(fragment: LoginFragmentSigninUsername2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(AccountCreatedFragment::class)
|
||||
fun bindAccountCreatedFragment(fragment: AccountCreatedFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginFragmentSignupUsername2::class)
|
||||
fun bindLoginFragmentSignupUsername2(fragment: LoginFragmentSignupUsername2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginFragmentSigninPassword2::class)
|
||||
fun bindLoginFragmentSigninPassword2(fragment: LoginFragmentSigninPassword2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginFragmentSignupPassword2::class)
|
||||
fun bindLoginFragmentSignupPassword2(fragment: LoginFragmentSignupPassword2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginCaptchaFragment2::class)
|
||||
fun bindLoginCaptchaFragment2(fragment: LoginCaptchaFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginFragmentToAny2::class)
|
||||
fun bindLoginFragmentToAny2(fragment: LoginFragmentToAny2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginTermsFragment2::class)
|
||||
fun bindLoginTermsFragment2(fragment: LoginTermsFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginServerUrlFormFragment2::class)
|
||||
fun bindLoginServerUrlFormFragment2(fragment: LoginServerUrlFormFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginResetPasswordMailConfirmationFragment2::class)
|
||||
fun bindLoginResetPasswordMailConfirmationFragment2(fragment: LoginResetPasswordMailConfirmationFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginResetPasswordFragment2::class)
|
||||
fun bindLoginResetPasswordFragment2(fragment: LoginResetPasswordFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginResetPasswordSuccessFragment2::class)
|
||||
fun bindLoginResetPasswordSuccessFragment2(fragment: LoginResetPasswordSuccessFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginServerSelectionFragment2::class)
|
||||
fun bindLoginServerSelectionFragment2(fragment: LoginServerSelectionFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginSsoOnlyFragment2::class)
|
||||
fun bindLoginSsoOnlyFragment2(fragment: LoginSsoOnlyFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginSplashSignUpSignInSelectionFragment2::class)
|
||||
fun bindLoginSplashSignUpSignInSelectionFragment2(fragment: LoginSplashSignUpSignInSelectionFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginWebFragment2::class)
|
||||
fun bindLoginWebFragment2(fragment: LoginWebFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginGenericTextInputFormFragment2::class)
|
||||
fun bindLoginGenericTextInputFormFragment2(fragment: LoginGenericTextInputFormFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LoginWaitForEmailFragment2::class)
|
||||
fun bindLoginWaitForEmailFragment2(fragment: LoginWaitForEmailFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthLegacyStyleCaptchaFragment::class)
|
||||
|
|
|
@ -58,8 +58,6 @@ import im.vector.app.features.location.LocationSharingViewModel
|
|||
import im.vector.app.features.location.live.map.LiveLocationMapViewModel
|
||||
import im.vector.app.features.location.preview.LocationPreviewViewModel
|
||||
import im.vector.app.features.login.LoginViewModel
|
||||
import im.vector.app.features.login2.LoginViewModel2
|
||||
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
||||
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
|
||||
import im.vector.app.features.media.VectorAttachmentViewerViewModel
|
||||
import im.vector.app.features.onboarding.OnboardingViewModel
|
||||
|
@ -456,21 +454,11 @@ interface MavericksViewModelModule {
|
|||
@MavericksViewModelKey(MatrixToBottomSheetViewModel::class)
|
||||
fun matrixToBottomSheetViewModelFactory(factory: MatrixToBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(AccountCreatedViewModel::class)
|
||||
fun accountCreatedViewModelFactory(factory: AccountCreatedViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(OnboardingViewModel::class)
|
||||
fun onboardingViewModelFactory(factory: OnboardingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(LoginViewModel2::class)
|
||||
fun loginViewModel2Factory(factory: LoginViewModel2.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(LoginViewModel::class)
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
|
||||
/**
|
||||
* Parent Fragment for all the login/registration screens.
|
||||
*/
|
||||
abstract class AbstractLoginFragment2<VB : ViewBinding> : VectorBaseFragment<VB>(), OnBackPressed {
|
||||
|
||||
protected val loginViewModel: LoginViewModel2 by activityViewModel()
|
||||
|
||||
private var isResetPasswordStarted = false
|
||||
|
||||
// Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
|
||||
private var displayCancelDialog = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
context?.let {
|
||||
sharedElementEnterTransition = TransitionInflater.from(it).inflateTransition(android.R.transition.move)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
loginViewModel.observeViewEvents {
|
||||
handleLoginViewEvents(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents2) {
|
||||
when (loginViewEvents) {
|
||||
is LoginViewEvents2.Failure -> showFailure(loginViewEvents.throwable)
|
||||
else ->
|
||||
// This is handled by the Activity
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun showFailure(throwable: Throwable) {
|
||||
// Only the resumed Fragment can eventually show the error, to avoid multiple dialog display
|
||||
if (!isResumed) {
|
||||
return
|
||||
}
|
||||
|
||||
when (throwable) {
|
||||
is CancellationException ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
Unit
|
||||
is Failure.UnrecognizedCertificateFailure ->
|
||||
showUnrecognizedCertificateFailure(throwable)
|
||||
else ->
|
||||
onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
|
||||
// Ask the user to accept the certificate
|
||||
unrecognizedCertificateDialog.show(requireActivity(),
|
||||
failure.fingerprint,
|
||||
failure.url,
|
||||
object : UnrecognizedCertificateDialog.Callback {
|
||||
override fun onAccept() {
|
||||
// User accept the certificate
|
||||
loginViewModel.handle(LoginAction2.UserAcceptCertificate(failure.fingerprint))
|
||||
}
|
||||
|
||||
override fun onIgnore() {
|
||||
// Cannot happen in this case
|
||||
}
|
||||
|
||||
override fun onReject() {
|
||||
// Nothing to do in this case
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
open fun onError(throwable: Throwable) {
|
||||
super.showFailure(throwable)
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return when {
|
||||
displayCancelDialog && loginViewModel.isRegistrationStarted -> {
|
||||
// Ask for confirmation before cancelling the registration
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_signup_cancel_confirmation_title)
|
||||
.setMessage(R.string.login_signup_cancel_confirmation_content)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
displayCancelDialog = false
|
||||
vectorBaseActivity.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
|
||||
true
|
||||
}
|
||||
displayCancelDialog && isResetPasswordStarted -> {
|
||||
// Ask for confirmation before cancelling the reset password
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_reset_password_cancel_confirmation_title)
|
||||
.setMessage(R.string.login_reset_password_cancel_confirmation_content)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
displayCancelDialog = false
|
||||
vectorBaseActivity.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
resetViewModel()
|
||||
// Do not consume the Back event
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override fun invalidate() = withState(loginViewModel) { state ->
|
||||
// True when email is sent with success to the homeserver
|
||||
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
|
||||
|
||||
updateWithState(state)
|
||||
}
|
||||
|
||||
open fun updateWithState(state: LoginViewState2) {
|
||||
// No op by default
|
||||
}
|
||||
|
||||
// Reset any modification on the loginViewModel by the current fragment
|
||||
abstract fun resetViewModel()
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.login2
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.hasSso
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
|
||||
abstract class AbstractSSOLoginFragment2<VB : ViewBinding> : AbstractLoginFragment2<VB>() {
|
||||
|
||||
// For sso
|
||||
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||
private var customTabsClient: CustomTabsClient? = null
|
||||
private var customTabsSession: CustomTabsSession? = null
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
|
||||
|
||||
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
|
||||
if (packageName != null) {
|
||||
customTabsServiceConnection = object : CustomTabsServiceConnection() {
|
||||
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||
customTabsClient = client
|
||||
.also { it.warmup(0L) }
|
||||
prefetchIfNeeded()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
}
|
||||
.also {
|
||||
CustomTabsClient.bindCustomTabsService(
|
||||
requireContext(),
|
||||
// Despite the API, packageName cannot be null
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
customTabsServiceConnection?.let { requireContext().unbindService(it) }
|
||||
customTabsServiceConnection = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefetchUrl(url: String) {
|
||||
if (customTabsSession == null) {
|
||||
customTabsSession = customTabsClient?.newSession(null)
|
||||
}
|
||||
|
||||
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
|
||||
}
|
||||
|
||||
protected fun openInCustomTab(ssoUrl: String) {
|
||||
openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl)
|
||||
}
|
||||
|
||||
private fun prefetchIfNeeded() {
|
||||
withState(loginViewModel) { state ->
|
||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||
// in this case we can prefetch (not other cases for privacy concerns)
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { prefetchUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.login.LoginConfig
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
|
||||
sealed class LoginAction2 : VectorViewModelAction {
|
||||
// First action
|
||||
data class UpdateSignMode(val signMode: SignMode2) : LoginAction2()
|
||||
|
||||
// Signin, but user wants to choose a server
|
||||
object ChooseAServerForSignin : LoginAction2()
|
||||
|
||||
object EnterServerUrl : LoginAction2()
|
||||
object ChooseDefaultHomeServer : LoginAction2()
|
||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction2()
|
||||
data class LoginWithToken(val loginToken: String) : LoginAction2()
|
||||
data class WebLoginSuccess(val credentials: Credentials) : LoginAction2()
|
||||
data class InitWith(val loginConfig: LoginConfig?) : LoginAction2()
|
||||
data class ResetPassword(val email: String, val newPassword: String) : LoginAction2()
|
||||
object ResetPasswordMailConfirmed : LoginAction2()
|
||||
|
||||
// Username to Login or Register, depending on the signMode
|
||||
data class SetUserName(val username: String) : LoginAction2()
|
||||
|
||||
// Password to Login or Register, depending on the signMode
|
||||
data class SetUserPassword(val password: String) : LoginAction2()
|
||||
|
||||
// When user has selected a homeserver
|
||||
data class LoginWith(val login: String, val password: String) : LoginAction2()
|
||||
|
||||
// Register actions
|
||||
open class RegisterAction : LoginAction2()
|
||||
|
||||
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
|
||||
object SendAgainThreePid : RegisterAction()
|
||||
|
||||
// TODO Confirm Email (from link in the email, open in the phone, intercepted by the app)
|
||||
data class ValidateThreePid(val code: String) : RegisterAction()
|
||||
|
||||
data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction()
|
||||
object StopEmailValidationCheck : RegisterAction()
|
||||
|
||||
data class CaptchaDone(val captchaResponse: String) : RegisterAction()
|
||||
object AcceptTerms : RegisterAction()
|
||||
object RegisterDummy : RegisterAction()
|
||||
|
||||
// Reset actions
|
||||
open class ResetAction : LoginAction2()
|
||||
|
||||
object ResetHomeServerUrl : ResetAction()
|
||||
object ResetSignMode : ResetAction()
|
||||
object ResetSignin : ResetAction()
|
||||
object ResetSignup : ResetAction()
|
||||
object ResetResetPassword : ResetAction()
|
||||
|
||||
// Homeserver history
|
||||
object ClearHomeServerHistory : LoginAction2()
|
||||
|
||||
// For the soft logout case
|
||||
data class SetupSsoForSessionRecovery(
|
||||
val homeServerUrl: String,
|
||||
val deviceId: String,
|
||||
val ssoIdentityProviders: List<SsoIdentityProvider>?
|
||||
) : LoginAction2()
|
||||
|
||||
data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2()
|
||||
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2()
|
||||
|
||||
// Account customization is over
|
||||
object Finish : LoginAction2()
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.AssetReader
|
||||
import im.vector.app.databinding.FragmentLoginCaptchaBinding
|
||||
import im.vector.app.features.login.JavascriptResponse
|
||||
import im.vector.app.features.login.LoginCaptchaFragmentArgument
|
||||
import org.matrix.android.sdk.api.util.MatrixJsonParser
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
import java.util.Formatter
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to confirm he is not a robot.
|
||||
*/
|
||||
class LoginCaptchaFragment2 @Inject constructor(
|
||||
private val assetReader: AssetReader
|
||||
) : AbstractLoginFragment2<FragmentLoginCaptchaBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding {
|
||||
return FragmentLoginCaptchaBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val params: LoginCaptchaFragmentArgument by args()
|
||||
|
||||
private var isWebViewLoaded = false
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView(state: LoginViewState2) {
|
||||
views.loginCaptchaWevView.settings.javaScriptEnabled = true
|
||||
|
||||
val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html")
|
||||
|
||||
val html = Formatter().format(reCaptchaPage, params.siteKey).toString()
|
||||
val mime = "text/html"
|
||||
val encoding = "utf-8"
|
||||
|
||||
val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver")
|
||||
views.loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null)
|
||||
views.loginCaptchaWevView.requestLayout()
|
||||
|
||||
views.loginCaptchaWevView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
// Show loader
|
||||
views.loginCaptchaProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
// Hide loader
|
||||
views.loginCaptchaProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
|
||||
Timber.d("## onReceivedSslError() : ${error.certificate}")
|
||||
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setMessage(R.string.ssl_could_not_verify)
|
||||
.setPositiveButton(R.string.ssl_trust) { _, _ ->
|
||||
Timber.d("## onReceivedSslError() : the user trusted")
|
||||
handler.proceed()
|
||||
}
|
||||
.setNegativeButton(R.string.ssl_do_not_trust) { _, _ ->
|
||||
Timber.d("## onReceivedSslError() : the user did not trust")
|
||||
handler.cancel()
|
||||
}
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
handler.cancel()
|
||||
Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.")
|
||||
dialog.dismiss()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
// common error message
|
||||
private fun onError(errorMessage: String) {
|
||||
Timber.e("## onError() : $errorMessage")
|
||||
|
||||
// TODO
|
||||
// Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show()
|
||||
|
||||
// on error case, close this activity
|
||||
// runOnUiThread(Runnable { finish() })
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
||||
super.onReceivedHttpError(view, request, errorResponse)
|
||||
|
||||
if (request.url.toString().endsWith("favicon.ico")) {
|
||||
// Ignore this error
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
onError(errorResponse.reasonPhrase)
|
||||
} else {
|
||||
onError(errorResponse.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
@Suppress("DEPRECATION")
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
onError(description)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
|
||||
if (url?.startsWith("js:") == true) {
|
||||
var json = url.substring(3)
|
||||
var javascriptResponse: JavascriptResponse? = null
|
||||
|
||||
try {
|
||||
// URL decode
|
||||
json = URLDecoder.decode(json, "UTF-8")
|
||||
javascriptResponse = MatrixJsonParser.getMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading(): failed")
|
||||
}
|
||||
|
||||
val response = javascriptResponse?.response
|
||||
if (javascriptResponse?.action == "verifyCallback" && response != null) {
|
||||
loginViewModel.handle(LoginAction2.CaptchaDone(response))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
if (!isWebViewLoaded) {
|
||||
setupWebView(state)
|
||||
isWebViewLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.databinding.FragmentLoginSigninPassword2Binding
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* - the user is asked for password to sign in to a homeserver.
|
||||
* - He also can reset his password
|
||||
*/
|
||||
class LoginFragmentSigninPassword2 @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer
|
||||
) : AbstractSSOLoginFragment2<FragmentLoginSigninPassword2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding {
|
||||
return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupForgottenPasswordButton()
|
||||
setupAutoFill()
|
||||
|
||||
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupForgottenPasswordButton() {
|
||||
views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() }
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
loginViewModel.handle(LoginAction2.SetUserPassword(password))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
// Name and avatar
|
||||
views.loginWelcomeBack.text = getString(
|
||||
R.string.login_welcome_back,
|
||||
state.loginProfileInfo()?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier()
|
||||
)
|
||||
|
||||
avatarRenderer.render(
|
||||
profileInfo = state.loginProfileInfo() ?: LoginProfileInfo(state.userIdentifier(), null, null),
|
||||
imageView = views.loginUserIcon
|
||||
)
|
||||
|
||||
views.loginWelcomeBackWarning.isVisible = ((state.loginProfileInfo as? Fail)
|
||||
?.error as? Failure.ServerError)
|
||||
?.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
views.passwordField
|
||||
.textChanges()
|
||||
.map { it.isNotEmpty() }
|
||||
.onEach {
|
||||
views.passwordFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun forgetPasswordClicked() {
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// loginViewModel.handle(LoginAction2.ResetSignin)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable.isInvalidPassword() && spaceInPassword()) {
|
||||
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
||||
} else {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if password ends or starts with spaces.
|
||||
*/
|
||||
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.databinding.FragmentLoginSigninUsername2Binding
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* - the user is asked for its matrix ID, and have the possibility to open the screen to select a server.
|
||||
*/
|
||||
class LoginFragmentSigninUsername2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSigninUsername2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninUsername2Binding {
|
||||
return FragmentLoginSigninUsername2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupAutoFill()
|
||||
views.loginChooseAServer.setOnClickListener {
|
||||
loginViewModel.handle(LoginAction2.ChooseAServerForSignin)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val login = views.loginField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
loginViewModel.handle(LoginAction2.SetUserName(login))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.loginFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
views.loginField.textChanges()
|
||||
.map { it.trim().isNotEmpty() }
|
||||
.onEach {
|
||||
views.loginFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignin)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_FORBIDDEN &&
|
||||
throwable.error.message.isEmpty()) {
|
||||
// Login with email, but email unknown
|
||||
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
|
||||
} else {
|
||||
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.databinding.FragmentLoginSignupPassword2Binding
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* - the user is asked to choose a password to sign up to a homeserver.
|
||||
*/
|
||||
class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSignupPassword2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding {
|
||||
return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupAutoFill()
|
||||
|
||||
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(R.string.error_empty_field_choose_password)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
loginViewModel.handle(LoginAction2.SetUserPassword(password))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
views.passwordField.textChanges()
|
||||
.onEach { password ->
|
||||
views.passwordFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = password.isNotEmpty()
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
views.loginMatrixIdentifier.text = state.userIdentifier()
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSignupUsername2Binding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* - the user is asked for an identifier to sign up to a homeserver.
|
||||
* - SSO option are displayed if available
|
||||
*/
|
||||
class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSignupUsername2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupUsername2Binding {
|
||||
return FragmentLoginSignupUsername2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupAutoFill()
|
||||
setupSocialLoginButtons()
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocialLoginButtons() {
|
||||
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val login = views.loginField.text.toString().trim()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(R.string.error_empty_field_choose_user_name)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
loginViewModel.handle(LoginAction2.SetUserName(login))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.loginFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.loginSubtitle.text = getString(R.string.login_signup_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
|
||||
if (state.loginMode is LoginMode.SsoAndPassword) {
|
||||
views.loginSocialLoginContainer.isVisible = true
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
|
||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
views.loginSocialLoginContainer.isVisible = false
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
views.loginField.textChanges()
|
||||
.map { it.trim() }
|
||||
.onEach { text ->
|
||||
val isNotEmpty = text.isNotEmpty()
|
||||
views.loginFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = isNotEmpty
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* User want to sign in and has selected a server to do so
|
||||
* - the user is asked for login (or email) and password to sign in to a homeserver.
|
||||
* - He also can reset his password
|
||||
* - It also possible to use SSO if server support it in this screen
|
||||
*/
|
||||
class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSigninToAny2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninToAny2Binding {
|
||||
return FragmentLoginSigninToAny2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupForgottenPasswordButton()
|
||||
setupAutoFill()
|
||||
setupSocialLoginButtons()
|
||||
|
||||
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupForgottenPasswordButton() {
|
||||
views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() }
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocialLoginButtons() {
|
||||
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val login = views.loginField.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name)
|
||||
error++
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
loginViewModel.handle(LoginAction2.LoginWith(login, password))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.loginFieldTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.loginTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
|
||||
if (state.loginMode is LoginMode.SsoAndPassword) {
|
||||
views.loginSocialLoginContainer.isVisible = true
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
|
||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
views.loginSocialLoginContainer.isVisible = false
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
combine(
|
||||
views.loginField.textChanges().map { it.trim().isNotEmpty() },
|
||||
views.passwordField.textChanges().map { it.isNotEmpty() }
|
||||
) { isLoginNotEmpty, isPasswordNotEmpty ->
|
||||
isLoginNotEmpty && isPasswordNotEmpty
|
||||
}
|
||||
.onEach {
|
||||
views.loginFieldTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun forgetPasswordClicked() {
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// loginViewModel.handle(LoginAction2.ResetSignin)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Show M_WEAK_PASSWORD error in the password field
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_WEAK_PASSWORD) {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
} else {
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_FORBIDDEN &&
|
||||
throwable.error.message.isEmpty()) {
|
||||
// Login with email, but email unknown
|
||||
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
|
||||
} else {
|
||||
// Trick to display the error without text.
|
||||
views.loginFieldTil.error = " "
|
||||
if (throwable.isInvalidPassword() && spaceInPassword()) {
|
||||
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
||||
} else {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if password ends or starts with spaces.
|
||||
*/
|
||||
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.args
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginGenericTextInputForm2Binding
|
||||
import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
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.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked for a text input.
|
||||
*/
|
||||
class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginGenericTextInputForm2Binding>() {
|
||||
|
||||
private val params: LoginGenericTextInputFormFragmentArgument by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputForm2Binding {
|
||||
return FragmentLoginGenericTextInputForm2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
setupUi()
|
||||
setupSubmitButton()
|
||||
setupTil()
|
||||
setupAutoFill()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginGenericTextInputFormOtherButton.setOnClickListener { onOtherButtonClicked() }
|
||||
views.loginGenericTextInputFormSubmit.setOnClickListener { submit() }
|
||||
views.loginGenericTextInputFormLater.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginGenericTextInputFormTextInput.setAutofillHints(
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS
|
||||
TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTil() {
|
||||
views.loginGenericTextInputFormTextInput.textChanges()
|
||||
.onEach {
|
||||
views.loginGenericTextInputFormTil.error = null
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title_2)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice_2)
|
||||
// Text will be updated with the state
|
||||
views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory
|
||||
views.loginGenericTextInputFormNotice2.isVisible = false
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = false
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit)
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title_2)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice_2)
|
||||
// Text will be updated with the state
|
||||
views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory
|
||||
views.loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2))
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = false
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit)
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra)
|
||||
views.loginGenericTextInputFormMandatoryNotice.isVisible = false
|
||||
views.loginGenericTextInputFormNotice2.isVisible = false
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(R.string.login_msisdn_confirm_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = true
|
||||
views.loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again)
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOtherButtonClicked() {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
loginViewModel.handle(LoginAction2.SendAgainThreePid)
|
||||
}
|
||||
else -> {
|
||||
// Should not happen, button is not displayed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
val text = views.loginGenericTextInputFormTextInput.text.toString()
|
||||
|
||||
if (text.isEmpty()) {
|
||||
// Perform dummy action
|
||||
loginViewModel.handle(LoginAction2.RegisterDummy)
|
||||
} else {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Email(text)))
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
getCountryCodeOrShowError(text)?.let { countryCode ->
|
||||
loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
loginViewModel.handle(LoginAction2.ValidateThreePid(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginGenericTextInputFormSubmit.hideKeyboard()
|
||||
views.loginGenericTextInputFormSubmit.error = null
|
||||
}
|
||||
|
||||
private fun getCountryCodeOrShowError(text: String): String? {
|
||||
// We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693)
|
||||
if (text.startsWith("+")) {
|
||||
try {
|
||||
val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null)
|
||||
return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
|
||||
} catch (e: NumberParseException) {
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other)
|
||||
}
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international)
|
||||
}
|
||||
|
||||
// Error
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginGenericTextInputFormSubmit.isEnabled = false
|
||||
views.loginGenericTextInputFormTextInput.textChanges()
|
||||
.onEach { text ->
|
||||
views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(text)
|
||||
updateSubmitButtons(text)
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun updateSubmitButtons(text: CharSequence) {
|
||||
if (params.mandatory) {
|
||||
views.loginGenericTextInputFormSubmit.isVisible = true
|
||||
views.loginGenericTextInputFormLater.isVisible = false
|
||||
} else {
|
||||
views.loginGenericTextInputFormSubmit.isVisible = text.isNotEmpty()
|
||||
views.loginGenericTextInputFormLater.isVisible = text.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInputValid(input: CharSequence): Boolean {
|
||||
return if (input.isEmpty() && !params.mandatory) {
|
||||
true
|
||||
} else {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> input.isEmail()
|
||||
TextInputFormFragmentMode.SetMsisdn -> input.isNotBlank()
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> input.isNotBlank()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
if (throwable.is401()) {
|
||||
// This is normal use case, we go to the mail waiting screen
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")))
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
if (throwable.is401()) {
|
||||
// This is normal use case, we go to the enter code screen
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")))
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
when {
|
||||
throwable is Failure.SuccessError ->
|
||||
// The entered code is not correct
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct)
|
||||
throwable.is401() ->
|
||||
// It can happen if user request again the 3pid
|
||||
Unit
|
||||
else ->
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
views.loginGenericTextInputFormMandatoryNotice.text = when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> getString(R.string.login_set_email_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl())
|
||||
TextInputFormFragmentMode.SetMsisdn -> getString(R.string.login_set_msisdn_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl())
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.core.utils.autoResetTextInputLayoutErrors
|
||||
import im.vector.app.databinding.FragmentLoginResetPassword2Binding
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked for email and new password to reset his password.
|
||||
*/
|
||||
class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPassword2Binding>() {
|
||||
|
||||
// Show warning only once
|
||||
private var showWarning = true
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPassword2Binding {
|
||||
return FragmentLoginResetPassword2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupAutoFill()
|
||||
|
||||
autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil))
|
||||
|
||||
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.resetPasswordEmail.setAutofillHints(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS)
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.resetPasswordSubmit.setOnClickListener { submit() }
|
||||
combine(
|
||||
views.resetPasswordEmail.textChanges().map { it.isEmail() },
|
||||
views.passwordField.textChanges().map { it.isNotEmpty() }
|
||||
) { isEmail, isPasswordNotEmpty ->
|
||||
isEmail && isPasswordNotEmpty
|
||||
}
|
||||
.onEach {
|
||||
views.resetPasswordSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
var error = 0
|
||||
|
||||
val email = views.resetPasswordEmail.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
if (email.isEmpty()) {
|
||||
views.resetPasswordEmailTil.error = getString(R.string.auth_reset_password_missing_email)
|
||||
error++
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(R.string.login_please_choose_a_new_password)
|
||||
error++
|
||||
}
|
||||
|
||||
if (error > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (showWarning) {
|
||||
// Display a warning as Riot-Web does first
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_reset_password_warning_title)
|
||||
.setMessage(R.string.login_reset_password_warning_content)
|
||||
.setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ ->
|
||||
showWarning = false
|
||||
doSubmit()
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
doSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doSubmit() {
|
||||
val email = views.resetPasswordEmail.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
loginViewModel.handle(LoginAction2.ResetPassword(email, password))
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.resetPasswordSubmit.hideKeyboard()
|
||||
views.resetPasswordEmailTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetResetPassword)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure new password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmation2Binding
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to check their email and to click on a button once it's done.
|
||||
*/
|
||||
class LoginResetPasswordMailConfirmationFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPasswordMailConfirmation2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmation2Binding {
|
||||
return FragmentLoginResetPasswordMailConfirmation2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
views.resetPasswordMailConfirmationSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
loginViewModel.handle(LoginAction2.ResetPasswordMailConfirmed)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetResetPassword)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Link in email not yet clicked ?
|
||||
val message = if (throwable.is401()) {
|
||||
getString(R.string.auth_reset_password_error_unauthorized)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordSuccess2Binding
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, we confirm to the user that his password has been reset.
|
||||
*/
|
||||
class LoginResetPasswordSuccessFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPasswordSuccess2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordSuccess2Binding {
|
||||
return FragmentLoginResetPasswordSuccess2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
views.resetPasswordSuccessSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetResetPassword)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextWithColoredPart
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.databinding.FragmentLoginServerSelection2Binding
|
||||
import im.vector.app.features.login.EMS_LINK
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user will choose between matrix.org, or other type of homeserver.
|
||||
*/
|
||||
class LoginServerSelectionFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginServerSelection2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerSelection2Binding {
|
||||
return FragmentLoginServerSelection2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initViews()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
views.loginServerChoiceMatrixOrg.setOnClickListener { selectMatrixOrg() }
|
||||
views.loginServerChoiceOther.setOnClickListener { selectOther() }
|
||||
|
||||
views.loginServerChoiceEmsLearnMore.setTextWithColoredPart(
|
||||
fullTextRes = R.string.login_server_modular_learn_more_about_ems,
|
||||
coloredTextRes = R.string.login_server_modular_learn_more,
|
||||
underline = true
|
||||
)
|
||||
views.loginServerChoiceEmsLearnMore.setOnClickListener {
|
||||
openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUi(state: LoginViewState2) {
|
||||
when (state.signMode) {
|
||||
SignMode2.Unknown -> Unit
|
||||
SignMode2.SignUp -> {
|
||||
views.loginServerTitle.setText(R.string.login_please_choose_a_server)
|
||||
}
|
||||
SignMode2.SignIn -> {
|
||||
views.loginServerTitle.setText(R.string.login_please_select_your_server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectMatrixOrg() {
|
||||
views.loginServerChoiceMatrixOrg.isChecked = true
|
||||
loginViewModel.handle(LoginAction2.ChooseDefaultHomeServer)
|
||||
}
|
||||
|
||||
private fun selectOther() {
|
||||
views.loginServerChoiceOther.isChecked = true
|
||||
loginViewModel.handle(LoginAction2.EnterServerUrl)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
views.loginServerChoiceMatrixOrg.isChecked = false
|
||||
views.loginServerChoiceOther.isChecked = false
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
updateUi(state)
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.resources.BuildMeta
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* In this screen, the user is prompted to enter a homeserver url.
|
||||
*/
|
||||
class LoginServerUrlFormFragment2 @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
) : AbstractLoginFragment2<FragmentLoginServerUrlForm2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerUrlForm2Binding {
|
||||
return FragmentLoginServerUrlForm2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
setupHomeServerField()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginServerUrlFormClearHistory.setOnClickListener { clearHistory() }
|
||||
views.loginServerUrlFormSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupHomeServerField() {
|
||||
views.loginServerUrlFormHomeServerUrl.textChanges()
|
||||
.onEach {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
|
||||
views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
views.loginServerUrlFormHomeServerUrl.dismissDropDown()
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
val completions = state.knownCustomHomeServersUrls + if (buildMeta.isDebug) listOf("http://10.0.2.2:8080") else emptyList()
|
||||
views.loginServerUrlFormHomeServerUrl.setAdapter(
|
||||
ArrayAdapter(
|
||||
requireContext(),
|
||||
R.layout.item_completion_homeserver,
|
||||
completions
|
||||
)
|
||||
)
|
||||
views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
|
||||
.takeIf { completions.isNotEmpty() }
|
||||
?: TextInputLayout.END_ICON_NONE
|
||||
|
||||
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
|
||||
}
|
||||
|
||||
private fun clearHistory() {
|
||||
loginViewModel.handle(LoginAction2.ClearHomeServerHistory)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
// Static check of homeserver url, empty, malformed, etc.
|
||||
val serverUrl = views.loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol()
|
||||
|
||||
when {
|
||||
serverUrl.isBlank() -> {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
|
||||
}
|
||||
else -> {
|
||||
views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/)
|
||||
loginViewModel.handle(LoginAction2.UpdateHomeServer(serverUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginServerUrlFormSubmit.hideKeyboard()
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection &&
|
||||
throwable.ioException is UnknownHostException) {
|
||||
// Invalid homeserver?
|
||||
getString(R.string.login_error_homeserver_not_found)
|
||||
} else {
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_FORBIDDEN &&
|
||||
throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
|
||||
getString(R.string.login_registration_disabled)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.core.resources.BuildMeta
|
||||
import im.vector.app.databinding.FragmentLoginSplash2Binding
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to sign up or to sign in to the homeserver.
|
||||
* This is the new splash screen.
|
||||
*/
|
||||
class LoginSplashSignUpSignInSelectionFragment2 @Inject constructor(
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : AbstractLoginFragment2<FragmentLoginSplash2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplash2Binding {
|
||||
return FragmentLoginSplash2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginSignupSigninSignUp.setOnClickListener { signUp() }
|
||||
views.loginSignupSigninSignIn.setOnClickListener { signIn() }
|
||||
|
||||
if (buildMeta.isDebug || vectorPreferences.developerMode()) {
|
||||
views.loginSplashVersion.isVisible = true
|
||||
@SuppressLint("SetTextI18n")
|
||||
views.loginSplashVersion.text = "Version : ${buildMeta.versionName}\n" +
|
||||
"Branch: ${buildMeta.gitBranchName}\n" +
|
||||
"Build: ${buildMeta.buildNumber}"
|
||||
views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun signUp() {
|
||||
loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignUp))
|
||||
}
|
||||
|
||||
private fun signIn() {
|
||||
loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignIn))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignMode)
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to sign up or to sign in to the homeserver.
|
||||
*/
|
||||
class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSsoOnly2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSsoOnly2Binding {
|
||||
return FragmentLoginSsoOnly2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginSignupSigninSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
|
||||
private fun submit() = withState(loginViewModel) { state ->
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||
|
||||
/**
|
||||
* Transient events for Login.
|
||||
*/
|
||||
sealed class LoginViewEvents2 : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : LoginViewEvents2()
|
||||
|
||||
data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents2()
|
||||
object OutdatedHomeserver : LoginViewEvents2()
|
||||
|
||||
// Navigation event
|
||||
object OpenSigninPasswordScreen : LoginViewEvents2()
|
||||
object OpenSignupPasswordScreen : LoginViewEvents2()
|
||||
|
||||
object OpenSignInEnterIdentifierScreen : LoginViewEvents2()
|
||||
|
||||
object OpenSignUpChooseUsernameScreen : LoginViewEvents2()
|
||||
object OpenSignInWithAnythingScreen : LoginViewEvents2()
|
||||
|
||||
object OpenSsoOnlyScreen : LoginViewEvents2()
|
||||
|
||||
object OpenServerSelection : LoginViewEvents2()
|
||||
object OpenHomeServerUrlFormScreen : LoginViewEvents2()
|
||||
|
||||
object OpenResetPasswordScreen : LoginViewEvents2()
|
||||
object OnResetPasswordSendThreePidDone : LoginViewEvents2()
|
||||
object OnResetPasswordMailConfirmationSuccess : LoginViewEvents2()
|
||||
object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents2()
|
||||
|
||||
object CancelRegistration : LoginViewEvents2()
|
||||
|
||||
data class OnLoginModeNotSupported(val supportedTypes: List<String>) : LoginViewEvents2()
|
||||
|
||||
data class OnSendEmailSuccess(val email: String) : LoginViewEvents2()
|
||||
data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents2()
|
||||
|
||||
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2()
|
||||
|
||||
data class OnSessionCreated(val newAccount: Boolean) : LoginViewEvents2()
|
||||
|
||||
object Finish : LoginViewEvents2()
|
||||
}
|
|
@ -1,829 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.configureAndStart
|
||||
import im.vector.app.core.extensions.tryAsync
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.ensureTrailingSlash
|
||||
import im.vector.app.features.login.HomeServerConnectionConfigFactory
|
||||
import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class LoginViewModel2 @AssistedInject constructor(
|
||||
@Assisted initialState: LoginViewState2,
|
||||
private val applicationContext: Context,
|
||||
private val authenticationService: AuthenticationService,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val stringProvider: StringProvider,
|
||||
private val homeServerHistoryService: HomeServerHistoryService
|
||||
) : VectorViewModel<LoginViewState2, LoginAction2, LoginViewEvents2>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<LoginViewModel2, LoginViewState2> {
|
||||
override fun create(initialState: LoginViewState2): LoginViewModel2
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<LoginViewModel2, LoginViewState2> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
getKnownCustomHomeServersUrls()
|
||||
}
|
||||
|
||||
private fun getKnownCustomHomeServersUrls() {
|
||||
setState {
|
||||
copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls())
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last action, to redo it after user has trusted the untrusted certificate
|
||||
private var lastAction: LoginAction2? = null
|
||||
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||
|
||||
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
||||
|
||||
val currentThreePid: String?
|
||||
get() = registrationWizard?.getCurrentThreePid()
|
||||
|
||||
// True when login and password has been sent with success to the homeserver
|
||||
val isRegistrationStarted: Boolean
|
||||
get() = authenticationService.isRegistrationStarted()
|
||||
|
||||
private val registrationWizard: RegistrationWizard?
|
||||
get() = authenticationService.getRegistrationWizard()
|
||||
|
||||
private val loginWizard: LoginWizard?
|
||||
get() = authenticationService.getLoginWizard()
|
||||
|
||||
private var loginConfig: LoginConfig? = null
|
||||
|
||||
private var currentJob: Job? = null
|
||||
set(value) {
|
||||
// Cancel any previous Job
|
||||
field?.cancel()
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun handle(action: LoginAction2) {
|
||||
when (action) {
|
||||
is LoginAction2.EnterServerUrl -> handleEnterServerUrl()
|
||||
is LoginAction2.ChooseAServerForSignin -> handleChooseAServerForSignin()
|
||||
is LoginAction2.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is LoginAction2.InitWith -> handleInitWith(action)
|
||||
is LoginAction2.ChooseDefaultHomeServer -> handle(LoginAction2.UpdateHomeServer(matrixOrgUrl))
|
||||
is LoginAction2.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
|
||||
is LoginAction2.SetUserName -> handleSetUserName(action).also { lastAction = action }
|
||||
is LoginAction2.SetUserPassword -> handleSetUserPassword(action).also { lastAction = action }
|
||||
is LoginAction2.LoginWith -> handleLoginWith(action).also { lastAction = action }
|
||||
is LoginAction2.LoginWithToken -> handleLoginWithToken(action)
|
||||
is LoginAction2.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is LoginAction2.ResetPassword -> handleResetPassword(action)
|
||||
is LoginAction2.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||
is LoginAction2.RegisterAction -> handleRegisterAction(action)
|
||||
is LoginAction2.ResetAction -> handleResetAction(action)
|
||||
is LoginAction2.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
|
||||
is LoginAction2.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
||||
LoginAction2.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
||||
is LoginAction2.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
||||
is LoginAction2.Finish -> handleFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFinish() {
|
||||
// Just post a view Event
|
||||
_viewEvents.post(LoginViewEvents2.Finish)
|
||||
}
|
||||
|
||||
private fun handleChooseAServerForSignin() {
|
||||
// Just post a view Event
|
||||
_viewEvents.post(LoginViewEvents2.OpenServerSelection)
|
||||
}
|
||||
|
||||
private fun handleUserAcceptCertificate(action: LoginAction2.UserAcceptCertificate) {
|
||||
// It happens when we get the login flow, or during direct authentication.
|
||||
// So alter the homeserver config and retrieve again the login flow
|
||||
when (val finalLastAction = lastAction) {
|
||||
is LoginAction2.UpdateHomeServer -> {
|
||||
currentHomeServerConnectionConfig
|
||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { getLoginFlow(it) }
|
||||
}
|
||||
is LoginAction2.SetUserName ->
|
||||
handleSetUserNameForSignIn(
|
||||
finalLastAction,
|
||||
HomeServerConnectionConfig.Builder()
|
||||
// Will be replaced by the task
|
||||
.withHomeServerUri("https://dummy.org")
|
||||
.withAllowedFingerPrints(listOf(action.fingerprint))
|
||||
.build()
|
||||
)
|
||||
is LoginAction2.SetUserPassword ->
|
||||
handleSetUserPassword(finalLastAction)
|
||||
is LoginAction2.LoginWith ->
|
||||
handleLoginWith(finalLastAction)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun rememberHomeServer(homeServerUrl: String) {
|
||||
homeServerHistoryService.addHomeServerToHistory(homeServerUrl)
|
||||
getKnownCustomHomeServersUrls()
|
||||
}
|
||||
|
||||
private fun handleClearHomeServerHistory() {
|
||||
homeServerHistoryService.clearHistory()
|
||||
getKnownCustomHomeServersUrls()
|
||||
}
|
||||
|
||||
private fun handleLoginWithToken(action: LoginAction2.LoginWithToken) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.loginWithToken(action.loginToken)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
null
|
||||
}
|
||||
?.let { onSessionCreated(it) }
|
||||
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetupSsoForSessionRecovery(action: LoginAction2.SetupSsoForSessionRecovery) {
|
||||
setState {
|
||||
copy(
|
||||
signMode = SignMode2.SignIn,
|
||||
loginMode = LoginMode.Sso(action.ssoIdentityProviders),
|
||||
homeServerUrlFromUser = action.homeServerUrl,
|
||||
homeServerUrl = action.homeServerUrl,
|
||||
deviceId = action.deviceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterAction(action: LoginAction2.RegisterAction) {
|
||||
when (action) {
|
||||
is LoginAction2.CaptchaDone -> handleCaptchaDone(action)
|
||||
is LoginAction2.AcceptTerms -> handleAcceptTerms()
|
||||
is LoginAction2.RegisterDummy -> handleRegisterDummy()
|
||||
is LoginAction2.AddThreePid -> handleAddThreePid(action)
|
||||
is LoginAction2.SendAgainThreePid -> handleSendAgainThreePid()
|
||||
is LoginAction2.ValidateThreePid -> handleValidateThreePid(action)
|
||||
is LoginAction2.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
|
||||
is LoginAction2.StopEmailValidationCheck -> handleStopEmailValidationCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckIfEmailHasBeenValidated(action: LoginAction2.CheckIfEmailHasBeenValidated) {
|
||||
// We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
|
||||
currentJob = executeRegistrationStep(withLoading = false) {
|
||||
it.checkIfEmailHasBeenValidated(action.delayMillis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStopEmailValidationCheck() {
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
private fun handleValidateThreePid(action: LoginAction2.ValidateThreePid) {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.handleValidateThreePid(action.code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeRegistrationStep(
|
||||
withLoading: Boolean = true,
|
||||
block: suspend (RegistrationWizard) -> RegistrationResult
|
||||
): Job {
|
||||
if (withLoading) {
|
||||
setState { copy(isLoading = true) }
|
||||
}
|
||||
return viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.let { block(it) }
|
||||
} catch (failure: Throwable) {
|
||||
if (failure !is CancellationException) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
}
|
||||
null
|
||||
}
|
||||
?.let { data ->
|
||||
when (data) {
|
||||
is RegistrationResult.Success -> onSessionCreated(data.session)
|
||||
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
|
||||
}
|
||||
}
|
||||
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddThreePid(action: LoginAction2.AddThreePid) {
|
||||
setState { copy(isLoading = true) }
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.addThreePid(action.threePid)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendAgainThreePid() {
|
||||
setState { copy(isLoading = true) }
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.sendAgainThreePid()
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAcceptTerms() {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.acceptTerms()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterDummy() {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.dummy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the user name is available.
|
||||
*/
|
||||
private fun handleSetUserNameForSignUp(action: LoginAction2.SetUserName) {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
val safeRegistrationWizard = registrationWizard ?: error("Invalid")
|
||||
|
||||
viewModelScope.launch {
|
||||
val available = safeRegistrationWizard.registrationAvailable(action.username)
|
||||
|
||||
val event = when (available) {
|
||||
RegistrationAvailability.Available -> {
|
||||
// Ask for a password
|
||||
LoginViewEvents2.OpenSignupPasswordScreen
|
||||
}
|
||||
is RegistrationAvailability.NotAvailable -> {
|
||||
LoginViewEvents2.Failure(available.failure)
|
||||
}
|
||||
}
|
||||
_viewEvents.post(event)
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCaptchaDone(action: LoginAction2.CaptchaDone) {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.performReCaptcha(action.captchaResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Update this
|
||||
private fun handleResetAction(action: LoginAction2.ResetAction) {
|
||||
// Cancel any request
|
||||
currentJob = null
|
||||
|
||||
when (action) {
|
||||
LoginAction2.ResetHomeServerUrl -> {
|
||||
viewModelScope.launch {
|
||||
authenticationService.reset()
|
||||
setState {
|
||||
copy(
|
||||
homeServerUrlFromUser = null,
|
||||
homeServerUrl = null,
|
||||
loginMode = LoginMode.Unknown
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LoginAction2.ResetSignMode -> {
|
||||
setState {
|
||||
copy(
|
||||
signMode = SignMode2.Unknown,
|
||||
loginMode = LoginMode.Unknown
|
||||
)
|
||||
}
|
||||
}
|
||||
LoginAction2.ResetSignin -> {
|
||||
viewModelScope.launch {
|
||||
authenticationService.cancelPendingLoginOrRegistration()
|
||||
setState {
|
||||
copy(isLoading = false)
|
||||
}
|
||||
}
|
||||
_viewEvents.post(LoginViewEvents2.CancelRegistration)
|
||||
}
|
||||
LoginAction2.ResetSignup -> {
|
||||
viewModelScope.launch {
|
||||
authenticationService.cancelPendingLoginOrRegistration()
|
||||
setState {
|
||||
// Always create a new state, to ensure the state is correctly reset
|
||||
LoginViewState2(
|
||||
knownCustomHomeServersUrls = knownCustomHomeServersUrls
|
||||
)
|
||||
}
|
||||
}
|
||||
_viewEvents.post(LoginViewEvents2.CancelRegistration)
|
||||
}
|
||||
LoginAction2.ResetResetPassword -> {
|
||||
setState {
|
||||
copy(
|
||||
resetPasswordEmail = null,
|
||||
resetPasswordNewPassword = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateSignMode(action: LoginAction2.UpdateSignMode) {
|
||||
setState {
|
||||
copy(
|
||||
signMode = action.signMode
|
||||
)
|
||||
}
|
||||
|
||||
when (action.signMode) {
|
||||
SignMode2.SignUp -> _viewEvents.post(LoginViewEvents2.OpenServerSelection)
|
||||
SignMode2.SignIn -> _viewEvents.post(LoginViewEvents2.OpenSignInEnterIdentifierScreen)
|
||||
SignMode2.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnterServerUrl() {
|
||||
_viewEvents.post(LoginViewEvents2.OpenHomeServerUrlFormScreen)
|
||||
}
|
||||
|
||||
private fun handleInitWith(action: LoginAction2.InitWith) {
|
||||
loginConfig = action.loginConfig
|
||||
|
||||
// If there is a pending email validation continue on this step
|
||||
try {
|
||||
if (registrationWizard?.isRegistrationStarted() == true) {
|
||||
currentThreePid?.let {
|
||||
handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(it)))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// NOOP. API is designed to use wizards in a login/registration flow,
|
||||
// but we need to check the state anyway.
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResetPassword(action: LoginAction2.ResetPassword) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.resetPassword(action.email)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
setState { copy(isLoading = false) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
resetPasswordEmail = action.email,
|
||||
resetPasswordNewPassword = action.newPassword
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(LoginViewEvents2.OnResetPasswordSendThreePidDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResetPasswordMailConfirmed() {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
val state = awaitState()
|
||||
safeLoginWizard.resetPasswordMailConfirmed(state.resetPasswordNewPassword!!)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
setState { copy(isLoading = false) }
|
||||
return@launch
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
resetPasswordEmail = null,
|
||||
resetPasswordNewPassword = null
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(LoginViewEvents2.OnResetPasswordMailConfirmationSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetUserName(action: LoginAction2.SetUserName) = withState { state ->
|
||||
setState {
|
||||
copy(
|
||||
userName = action.username
|
||||
)
|
||||
}
|
||||
|
||||
when (state.signMode) {
|
||||
SignMode2.Unknown -> error("Developer error, invalid sign mode")
|
||||
SignMode2.SignIn -> handleSetUserNameForSignIn(action, null)
|
||||
SignMode2.SignUp -> handleSetUserNameForSignUp(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetUserPassword(action: LoginAction2.SetUserPassword) = withState { state ->
|
||||
when (state.signMode) {
|
||||
SignMode2.Unknown -> error("Developer error, invalid sign mode")
|
||||
SignMode2.SignIn -> handleSignInWithPassword(action)
|
||||
SignMode2.SignUp -> handleRegisterWithPassword(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterWithPassword(action: LoginAction2.SetUserPassword) = withState { state ->
|
||||
val username = state.userName ?: error("Developer error, username not set")
|
||||
|
||||
reAuthHelper.data = action.password
|
||||
currentJob = executeRegistrationStep {
|
||||
it.createAccount(
|
||||
userName = username,
|
||||
password = action.password,
|
||||
initialDeviceDisplayName = stringProvider.getString(R.string.login_default_session_public_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSignInWithPassword(action: LoginAction2.SetUserPassword) = withState { state ->
|
||||
val username = state.userName ?: error("Developer error, username not set")
|
||||
setState { copy(isLoading = true) }
|
||||
loginWith(username, action.password)
|
||||
}
|
||||
|
||||
private fun handleLoginWith(action: LoginAction2.LoginWith) {
|
||||
setState { copy(isLoading = true) }
|
||||
loginWith(action.login, action.password)
|
||||
}
|
||||
|
||||
private fun loginWith(login: String, password: String) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
|
||||
setState { copy(isLoading = false) }
|
||||
} else {
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.login(
|
||||
login = login,
|
||||
password = password,
|
||||
initialDeviceName = stringProvider.getString(R.string.login_default_session_public_name)
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
null
|
||||
}
|
||||
?.let {
|
||||
reAuthHelper.data = password
|
||||
onSessionCreated(it)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform wellknown request.
|
||||
*/
|
||||
private fun handleSetUserNameForSignIn(action: LoginAction2.SetUserName, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
val data = try {
|
||||
authenticationService.getWellKnownData(action.username, homeServerConnectionConfig)
|
||||
} catch (failure: Throwable) {
|
||||
onDirectLoginError(failure)
|
||||
return@launch
|
||||
}
|
||||
when (data) {
|
||||
is WellknownResult.Prompt ->
|
||||
onWellknownSuccess(action, data, homeServerConnectionConfig)
|
||||
is WellknownResult.FailPrompt ->
|
||||
// Relax on IS discovery if homeserver is valid
|
||||
if (data.homeServerUrl != null && data.wellKnown != null) {
|
||||
onWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig)
|
||||
} else {
|
||||
onWellKnownError()
|
||||
}
|
||||
else -> {
|
||||
onWellKnownError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onWellKnownError() {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))))
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
|
||||
private suspend fun onWellknownSuccess(
|
||||
action: LoginAction2.SetUserName,
|
||||
wellKnownPrompt: WellknownResult.Prompt,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig?
|
||||
) {
|
||||
val alteredHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
?.copy(
|
||||
homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
?: HomeServerConnectionConfig(
|
||||
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
|
||||
// Ensure login flow is retrieved, and this is not a SSO only server
|
||||
val data = try {
|
||||
authenticationService.getLoginFlow(alteredHomeServerConnectionConfig)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
null
|
||||
} ?: return
|
||||
|
||||
val loginMode = when {
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
||||
val viewEvent = when (loginMode) {
|
||||
LoginMode.Password,
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
retrieveProfileInfo(action.username)
|
||||
// We can navigate to the password screen
|
||||
LoginViewEvents2.OpenSigninPasswordScreen
|
||||
}
|
||||
is LoginMode.Sso -> {
|
||||
LoginViewEvents2.OpenSsoOnlyScreen
|
||||
}
|
||||
LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList())
|
||||
LoginMode.Unknown -> null
|
||||
}
|
||||
viewEvent?.let { _viewEvents.post(it) }
|
||||
|
||||
val urlFromUser = action.username.getServerName()
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
homeServerUrlFromUser = urlFromUser,
|
||||
homeServerUrl = data.homeServerUrl,
|
||||
loginMode = loginMode
|
||||
)
|
||||
}
|
||||
|
||||
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) ||
|
||||
data.isOutdatedHomeserver) {
|
||||
// Notify the UI
|
||||
_viewEvents.post(LoginViewEvents2.OutdatedHomeserver)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieveProfileInfo(username: String) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard != null) {
|
||||
setState { copy(loginProfileInfo = Loading()) }
|
||||
val result = tryAsync {
|
||||
safeLoginWizard.getProfileInfo(username)
|
||||
}
|
||||
setState { copy(loginProfileInfo = result) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDirectLoginError(failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
|
||||
private fun onFlowResponse(flowResult: FlowResult) {
|
||||
// If dummy stage is mandatory, and password is already sent, do the dummy stage now
|
||||
if (isRegistrationStarted &&
|
||||
flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
|
||||
handleRegisterDummy()
|
||||
} else {
|
||||
// Notify the user
|
||||
_viewEvents.post(LoginViewEvents2.RegistrationFlowResult(flowResult, isRegistrationStarted))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onSessionCreated(session: Session) {
|
||||
activeSessionHolder.setActiveSession(session)
|
||||
|
||||
authenticationService.reset()
|
||||
session.configureAndStart(applicationContext)
|
||||
withState { state ->
|
||||
_viewEvents.post(LoginViewEvents2.OnSessionCreated(state.signMode == SignMode2.SignUp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWebLoginSuccess(action: LoginAction2.WebLoginSuccess) = withState { state ->
|
||||
val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl)
|
||||
|
||||
if (homeServerConnectionConfigFinal == null) {
|
||||
// Should not happen
|
||||
Timber.w("homeServerConnectionConfig is null")
|
||||
} else {
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
null
|
||||
}
|
||||
?.let { onSessionCreated(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateHomeserver(action: LoginAction2.UpdateHomeServer) {
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
||||
if (homeServerConnectionConfig == null) {
|
||||
// This is invalid
|
||||
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||
} else {
|
||||
getLoginFlow(homeServerConnectionConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) = withState { state ->
|
||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
authenticationService.cancelPendingLoginOrRegistration()
|
||||
|
||||
val data = try {
|
||||
authenticationService.getLoginFlow(homeServerConnectionConfig)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||
setState { copy(isLoading = false) }
|
||||
null
|
||||
} ?: return@launch
|
||||
|
||||
// Valid Homeserver, add it to the history.
|
||||
// Note: we add what the user has input, data.homeServerUrlBase can be different
|
||||
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
|
||||
|
||||
val loginMode = when {
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
||||
val viewEvent = when (loginMode) {
|
||||
LoginMode.Password,
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
when (state.signMode) {
|
||||
SignMode2.Unknown -> null
|
||||
SignMode2.SignUp -> {
|
||||
// Check that registration is possible on this server
|
||||
try {
|
||||
registrationWizard?.getRegistrationFlow()
|
||||
|
||||
/*
|
||||
// Simulate registration disabled
|
||||
throw Failure.ServerError(
|
||||
error = MatrixError(
|
||||
code = MatrixError.M_FORBIDDEN,
|
||||
message = "Registration is disabled"
|
||||
),
|
||||
httpCode = 403
|
||||
)
|
||||
*/
|
||||
|
||||
LoginViewEvents2.OpenSignUpChooseUsernameScreen
|
||||
} catch (throwable: Throwable) {
|
||||
// Registration disabled?
|
||||
LoginViewEvents2.Failure(throwable)
|
||||
}
|
||||
}
|
||||
SignMode2.SignIn -> LoginViewEvents2.OpenSignInWithAnythingScreen
|
||||
}
|
||||
}
|
||||
is LoginMode.Sso -> {
|
||||
LoginViewEvents2.OpenSsoOnlyScreen
|
||||
}
|
||||
LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList())
|
||||
LoginMode.Unknown -> null
|
||||
}
|
||||
viewEvent?.let { _viewEvents.post(it) }
|
||||
|
||||
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) ||
|
||||
data.isOutdatedHomeserver) {
|
||||
// Notify the UI
|
||||
_viewEvents.post(LoginViewEvents2.OutdatedHomeserver)
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(),
|
||||
homeServerUrl = data.homeServerUrl,
|
||||
loginMode = loginMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getInitialHomeServerUrl(): String? {
|
||||
return loginConfig?.homeServerUrl
|
||||
}
|
||||
|
||||
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? {
|
||||
return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId)
|
||||
}
|
||||
|
||||
fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {
|
||||
return authenticationService.getFallbackUrl(forSignIn, deviceId)
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.PersistState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
|
||||
|
||||
data class LoginViewState2(
|
||||
val isLoading: Boolean = false,
|
||||
|
||||
// User choices
|
||||
@PersistState
|
||||
val signMode: SignMode2 = SignMode2.Unknown,
|
||||
@PersistState
|
||||
val userName: String? = null,
|
||||
@PersistState
|
||||
val resetPasswordEmail: String? = null,
|
||||
@PersistState
|
||||
val resetPasswordNewPassword: String? = null,
|
||||
@PersistState
|
||||
val homeServerUrlFromUser: String? = null,
|
||||
|
||||
// Can be modified after a Wellknown request
|
||||
@PersistState
|
||||
val homeServerUrl: String? = null,
|
||||
|
||||
// For SSO session recovery
|
||||
@PersistState
|
||||
val deviceId: String? = null,
|
||||
|
||||
// Network result
|
||||
val loginProfileInfo: Async<LoginProfileInfo> = Uninitialized,
|
||||
|
||||
// Network result
|
||||
@PersistState
|
||||
val loginMode: LoginMode = LoginMode.Unknown,
|
||||
|
||||
// From database
|
||||
val knownCustomHomeServersUrls: List<String> = emptyList()
|
||||
) : MavericksState {
|
||||
|
||||
// Pending user identifier
|
||||
fun userIdentifier(): String {
|
||||
return if (userName != null && MatrixPatterns.isUserId(userName)) {
|
||||
userName
|
||||
} else {
|
||||
"@$userName:${homeServerUrlFromUser.toReducedUrl()}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.args
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginWaitForEmail2Binding
|
||||
import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to check their emails.
|
||||
*/
|
||||
class LoginWaitForEmailFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginWaitForEmail2Binding>() {
|
||||
|
||||
private val params: LoginWaitForEmailFragmentArgument by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmail2Binding {
|
||||
return FragmentLoginWaitForEmail2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupUi()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(0))
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
loginViewModel.handle(LoginAction2.StopEmailValidationCheck)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice_2, params.email)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable.is401()) {
|
||||
// Try again, with a delay
|
||||
loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(10_000))
|
||||
} else {
|
||||
super.onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.AssetReader
|
||||
import im.vector.app.databinding.FragmentLoginWebBinding
|
||||
import im.vector.app.features.login.JavascriptResponse
|
||||
import im.vector.app.features.signout.soft.SoftLogoutAction
|
||||
import im.vector.app.features.signout.soft.SoftLogoutViewModel
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.util.MatrixJsonParser
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This screen is displayed when the application does not support login flow or registration flow
|
||||
* of the homeserver, as a fallback to login or to create an account.
|
||||
*/
|
||||
class LoginWebFragment2 @Inject constructor(
|
||||
private val assetReader: AssetReader
|
||||
) : AbstractLoginFragment2<FragmentLoginWebBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWebBinding {
|
||||
return FragmentLoginWebBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
|
||||
|
||||
private var isWebViewLoaded = false
|
||||
private var isForSessionRecovery = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupToolbar(views.loginWebToolbar)
|
||||
.allowBack()
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupTitle(state)
|
||||
|
||||
isForSessionRecovery = state.deviceId?.isNotBlank() == true
|
||||
|
||||
if (!isWebViewLoaded) {
|
||||
setupWebView(state)
|
||||
isWebViewLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTitle(state: LoginViewState2) {
|
||||
toolbar?.title = when (state.signMode) {
|
||||
SignMode2.SignIn -> getString(R.string.login_signin)
|
||||
else -> getString(R.string.login_signup)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView(state: LoginViewState2) {
|
||||
views.loginWebWebView.settings.javaScriptEnabled = true
|
||||
|
||||
// Enable local storage to support SSO with Firefox accounts
|
||||
views.loginWebWebView.settings.domStorageEnabled = true
|
||||
views.loginWebWebView.settings.databaseEnabled = true
|
||||
|
||||
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
|
||||
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
|
||||
views.loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google"
|
||||
|
||||
// AppRTC requires third party cookies to work
|
||||
val cookieManager = android.webkit.CookieManager.getInstance()
|
||||
|
||||
// clear the cookies
|
||||
if (cookieManager == null) {
|
||||
launchWebView(state)
|
||||
} else {
|
||||
if (!cookieManager.hasCookies()) {
|
||||
launchWebView(state)
|
||||
} else {
|
||||
try {
|
||||
cookieManager.removeAllCookies { launchWebView(state) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||
launchWebView(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWebView(state: LoginViewState2) {
|
||||
val url = loginViewModel.getFallbackUrl(state.signMode == SignMode2.SignIn, state.deviceId) ?: return
|
||||
|
||||
views.loginWebWebView.loadUrl(url)
|
||||
|
||||
views.loginWebWebView.webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedSslError(
|
||||
view: WebView,
|
||||
handler: SslErrorHandler,
|
||||
error: SslError
|
||||
) {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setMessage(R.string.ssl_could_not_verify)
|
||||
.setPositiveButton(R.string.ssl_trust) { _, _ -> handler.proceed() }
|
||||
.setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> handler.cancel() }
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
handler.cancel()
|
||||
dialog.dismiss()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnWebLoginError(errorCode, description, failingUrl)))
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
toolbar?.subtitle = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// avoid infinite onPageFinished call
|
||||
if (url.startsWith("http")) {
|
||||
// Generic method to make a bridge between JS and the UIWebView
|
||||
assetReader.readAssetFile("sendObject.js")?.let { view.loadUrl(it) }
|
||||
|
||||
if (state.signMode == SignMode2.SignIn) {
|
||||
// The function the fallback page calls when the login is complete
|
||||
assetReader.readAssetFile("onLogin.js")?.let { view.loadUrl(it) }
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// The function the fallback page calls when the registration is complete
|
||||
assetReader.readAssetFile("onRegistered.js")?.let { view.loadUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of (formatted) url for MODE_LOGIN:
|
||||
*
|
||||
* <pre>
|
||||
* js:{
|
||||
* "action":"onLogin",
|
||||
* "credentials":{
|
||||
* "user_id":"@user:matrix.org",
|
||||
* "access_token":"[ACCESS_TOKEN]",
|
||||
* "home_server":"matrix.org",
|
||||
* "device_id":"[DEVICE_ID]",
|
||||
* "well_known":{
|
||||
* "m.homeserver":{
|
||||
* "base_url":"https://matrix.org/"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* .
|
||||
* </pre>
|
||||
* @param view
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||
if (url == null) return super.shouldOverrideUrlLoading(view, url as String?)
|
||||
|
||||
if (url.startsWith("js:")) {
|
||||
var json = url.substring(3)
|
||||
var javascriptResponse: JavascriptResponse? = null
|
||||
|
||||
try {
|
||||
// URL decode
|
||||
json = URLDecoder.decode(json, "UTF-8")
|
||||
val adapter = MatrixJsonParser.getMoshi().adapter(JavascriptResponse::class.java)
|
||||
javascriptResponse = adapter.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
|
||||
}
|
||||
|
||||
// succeeds to parse parameters
|
||||
if (javascriptResponse != null) {
|
||||
val action = javascriptResponse.action
|
||||
|
||||
if (state.signMode == SignMode2.SignIn) {
|
||||
if (action == "onLogin") {
|
||||
javascriptResponse.credentials?.let { notifyViewModel(it) }
|
||||
}
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// check the required parameters
|
||||
if (action == "onRegistered") {
|
||||
javascriptResponse.credentials?.let { notifyViewModel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return super.shouldOverrideUrlLoading(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyViewModel(credentials: Credentials) {
|
||||
if (isForSessionRecovery) {
|
||||
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
|
||||
} else {
|
||||
loginViewModel.handle(LoginAction2.WebLoginSuccess(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignin)
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return when {
|
||||
toolbarButton -> super.onBackPressed(toolbarButton)
|
||||
views.loginWebWebView.canGoBack() -> views.loginWebWebView.goBack().run { true }
|
||||
else -> super.onBackPressed(toolbarButton)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.login2
|
||||
|
||||
enum class SignMode2 {
|
||||
Unknown,
|
||||
|
||||
// Account creation
|
||||
SignUp,
|
||||
|
||||
// Login
|
||||
SignIn
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.login2.created
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class AccountCreatedAction : VectorViewModelAction {
|
||||
data class SetDisplayName(val displayName: String) : AccountCreatedAction()
|
||||
data class SetAvatar(val avatarUri: Uri, val filename: String) : AccountCreatedAction()
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.login2.created
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.databinding.DialogBaseEditTextBinding
|
||||
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.login2.AbstractLoginFragment2
|
||||
import im.vector.app.features.login2.LoginAction2
|
||||
import im.vector.app.features.login2.LoginViewState2
|
||||
import im.vector.app.features.onboarding.OnboardingActivity
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* - the account has been created and we propose the user to set an avatar and a display name.
|
||||
*/
|
||||
class AccountCreatedFragment @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val clock: Clock,
|
||||
colorProvider: ColorProvider
|
||||
) : AbstractLoginFragment2<FragmentLoginAccountCreatedBinding>(),
|
||||
GalleryOrCameraDialogHelper.Listener {
|
||||
|
||||
private val viewModel: AccountCreatedViewModel by fragmentViewModel()
|
||||
|
||||
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock)
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding {
|
||||
return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupClickListener()
|
||||
setupSubmitButton()
|
||||
observeViewEvents()
|
||||
|
||||
viewModel.onEach { invalidateState(it) }
|
||||
|
||||
views.loginAccountCreatedTime.text = dateFormatter.format(clock.epochMillis(), DateFormatKind.MESSAGE_SIMPLE)
|
||||
}
|
||||
|
||||
private fun observeViewEvents() {
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is AccountCreatedViewEvents.Failure -> displayErrorDialog(it.throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListener() {
|
||||
views.loginAccountCreatedMessage.debouncedClicks {
|
||||
// Update display name
|
||||
displayDialog()
|
||||
}
|
||||
views.loginAccountCreatedAvatar.debouncedClicks {
|
||||
galleryOrCameraDialogHelper.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayDialog() = withState(viewModel) { state ->
|
||||
val inflater = requireActivity().layoutInflater
|
||||
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
|
||||
val views = DialogBaseEditTextBinding.bind(layout)
|
||||
views.editText.setText(state.currentUser()?.getBestName().orEmpty())
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.settings_display_name)
|
||||
.setView(layout)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val newName = views.editText.text.toString()
|
||||
viewModel.handle(AccountCreatedAction.SetDisplayName(newName))
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri?) {
|
||||
uri ?: return
|
||||
viewModel.handle(
|
||||
AccountCreatedAction.SetAvatar(
|
||||
avatarUri = uri,
|
||||
filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginAccountCreatedLater.debouncedClicks { terminate() }
|
||||
views.loginAccountCreatedDone.debouncedClicks { terminate() }
|
||||
}
|
||||
|
||||
private fun terminate() {
|
||||
loginViewModel.handle(LoginAction2.Finish)
|
||||
}
|
||||
|
||||
private fun invalidateState(state: AccountCreatedViewState) {
|
||||
// Ugly hack...
|
||||
(activity as? OnboardingActivity)?.setIsLoading(state.isLoading)
|
||||
|
||||
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)
|
||||
|
||||
val user = state.currentUser()
|
||||
if (user != null) {
|
||||
avatarRenderer.render(user, views.loginAccountCreatedAvatar)
|
||||
views.loginAccountCreatedMemberName.text = user.getBestName()
|
||||
} else {
|
||||
// Should not happen
|
||||
views.loginAccountCreatedMemberName.text = state.userId
|
||||
}
|
||||
|
||||
// User color
|
||||
views.loginAccountCreatedMemberName
|
||||
.setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(state.userId)))
|
||||
|
||||
views.loginAccountCreatedLater.isVisible = state.hasBeenModified.not()
|
||||
views.loginAccountCreatedDone.isVisible = state.hasBeenModified
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
// Just start the next Activity
|
||||
terminate()
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 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.login2.created
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
/**
|
||||
* Transient events for Account Created.
|
||||
*/
|
||||
sealed class AccountCreatedViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : AccountCreatedViewEvents()
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.login2.created
|
||||
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
import org.matrix.android.sdk.flow.unwrap
|
||||
import timber.log.Timber
|
||||
|
||||
class AccountCreatedViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: AccountCreatedViewState,
|
||||
private val session: Session
|
||||
) : VectorViewModel<AccountCreatedViewState, AccountCreatedAction, AccountCreatedViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<AccountCreatedViewModel, AccountCreatedViewState> {
|
||||
override fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<AccountCreatedViewModel, AccountCreatedViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
setState {
|
||||
copy(
|
||||
userId = session.myUserId
|
||||
)
|
||||
}
|
||||
observeUser()
|
||||
}
|
||||
|
||||
private fun observeUser() {
|
||||
session.flow()
|
||||
.liveUser(session.myUserId)
|
||||
.unwrap()
|
||||
.map {
|
||||
if (MatrixPatterns.isUserId(it.userId)) {
|
||||
it.toMatrixItem()
|
||||
} else {
|
||||
Timber.w("liveUser() has returned an invalid user: $it")
|
||||
MatrixItem.UserItem(session.myUserId, null, null)
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
copy(currentUser = it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: AccountCreatedAction) {
|
||||
when (action) {
|
||||
is AccountCreatedAction.SetAvatar -> handleSetAvatar(action)
|
||||
is AccountCreatedAction.SetDisplayName -> handleSetDisplayName(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetAvatar(action: AccountCreatedAction.SetAvatar) {
|
||||
setState { copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
val result = runCatching { session.profileService().updateAvatar(session.myUserId, action.avatarUri, action.filename) }
|
||||
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
hasBeenModified = hasBeenModified || result.isSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetDisplayName(action: AccountCreatedAction.SetDisplayName) {
|
||||
setState { copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
val result = runCatching { session.profileService().setDisplayName(session.myUserId, action.displayName) }
|
||||
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
hasBeenModified = hasBeenModified || result.isSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.login2.created
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
data class AccountCreatedViewState(
|
||||
val userId: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val currentUser: Async<MatrixItem.UserItem> = Uninitialized,
|
||||
val hasBeenModified: Boolean = false
|
||||
) : MavericksState
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 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.login2.terms
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.args
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.databinding.FragmentLoginTerms2Binding
|
||||
import im.vector.app.features.login.terms.LocalizedFlowDataLoginTermsChecked
|
||||
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
||||
import im.vector.app.features.login.terms.LoginTermsViewState
|
||||
import im.vector.app.features.login.terms.PolicyController
|
||||
import im.vector.app.features.login2.AbstractLoginFragment2
|
||||
import im.vector.app.features.login2.LoginAction2
|
||||
import im.vector.app.features.login2.LoginViewState2
|
||||
import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* LoginTermsFragment displays the list of policies the user has to accept.
|
||||
*/
|
||||
class LoginTermsFragment2 @Inject constructor(
|
||||
private val policyController: PolicyController
|
||||
) : AbstractLoginFragment2<FragmentLoginTerms2Binding>(),
|
||||
PolicyController.PolicyControllerListener {
|
||||
|
||||
private val params: LoginTermsFragmentArgument by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTerms2Binding {
|
||||
return FragmentLoginTerms2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList())
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
views.loginTermsPolicyList.configureWith(policyController)
|
||||
policyController.listener = this
|
||||
|
||||
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
|
||||
|
||||
params.localizedFlowDataLoginTerms
|
||||
.forEach {
|
||||
list.add(LocalizedFlowDataLoginTermsChecked(it))
|
||||
}
|
||||
|
||||
loginTermsViewState = LoginTermsViewState(list)
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginTermsSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
views.loginTermsPolicyList.cleanup()
|
||||
policyController.listener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun renderState() {
|
||||
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
|
||||
|
||||
// Button is enabled only if all checkboxes are checked
|
||||
views.loginTermsSubmit.isEnabled = loginTermsViewState.allChecked()
|
||||
}
|
||||
|
||||
override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
loginTermsViewState.check(localizedFlowDataLoginTerms)
|
||||
} else {
|
||||
loginTermsViewState.uncheck(localizedFlowDataLoginTerms)
|
||||
}
|
||||
|
||||
renderState()
|
||||
}
|
||||
|
||||
override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) {
|
||||
localizedFlowDataLoginTerms.localizedUrl
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
openUrlInChromeCustomTab(requireContext(), null, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
loginViewModel.handle(LoginAction2.AcceptTerms)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
policyController.homeServer = state.homeServerUrlFromUser.toReducedUrl()
|
||||
renderState()
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetSignup)
|
||||
}
|
||||
}
|
|
@ -130,7 +130,6 @@ class DefaultNavigator @Inject constructor(
|
|||
override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) {
|
||||
val intent = when (features.onboardingVariant()) {
|
||||
OnboardingVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig)
|
||||
OnboardingVariant.LOGIN_2,
|
||||
OnboardingVariant.FTUE_AUTH -> OnboardingActivity.newIntent(context, loginConfig)
|
||||
}
|
||||
intent.addFlags(flags)
|
||||
|
@ -140,7 +139,6 @@ class DefaultNavigator @Inject constructor(
|
|||
override fun loginSSORedirect(context: Context, data: Uri?) {
|
||||
val intent = when (features.onboardingVariant()) {
|
||||
OnboardingVariant.LEGACY -> LoginActivity.redirectIntent(context, data)
|
||||
OnboardingVariant.LOGIN_2,
|
||||
OnboardingVariant.FTUE_AUTH -> OnboardingActivity.redirectIntent(context, data)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
|
|
@ -1,426 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.addFragmentToBackstack
|
||||
import im.vector.app.core.extensions.resetBackstack
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityLoginBinding
|
||||
import im.vector.app.features.home.HomeActivity
|
||||
import im.vector.app.features.login.LoginCaptchaFragmentArgument
|
||||
import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument
|
||||
import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
import im.vector.app.features.login.isSupported
|
||||
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
||||
import im.vector.app.features.login2.LoginAction2
|
||||
import im.vector.app.features.login2.LoginCaptchaFragment2
|
||||
import im.vector.app.features.login2.LoginFragmentSigninPassword2
|
||||
import im.vector.app.features.login2.LoginFragmentSigninUsername2
|
||||
import im.vector.app.features.login2.LoginFragmentSignupPassword2
|
||||
import im.vector.app.features.login2.LoginFragmentSignupUsername2
|
||||
import im.vector.app.features.login2.LoginFragmentToAny2
|
||||
import im.vector.app.features.login2.LoginGenericTextInputFormFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2
|
||||
import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2
|
||||
import im.vector.app.features.login2.LoginServerSelectionFragment2
|
||||
import im.vector.app.features.login2.LoginServerUrlFormFragment2
|
||||
import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2
|
||||
import im.vector.app.features.login2.LoginSsoOnlyFragment2
|
||||
import im.vector.app.features.login2.LoginViewEvents2
|
||||
import im.vector.app.features.login2.LoginViewModel2
|
||||
import im.vector.app.features.login2.LoginViewState2
|
||||
import im.vector.app.features.login2.LoginWaitForEmailFragment2
|
||||
import im.vector.app.features.login2.LoginWebFragment2
|
||||
import im.vector.app.features.login2.created.AccountCreatedFragment
|
||||
import im.vector.app.features.login2.terms.LoginTermsFragment2
|
||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
||||
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
|
||||
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
||||
|
||||
class Login2Variant(
|
||||
private val views: ActivityLoginBinding,
|
||||
private val loginViewModel: LoginViewModel2,
|
||||
private val activity: VectorBaseActivity<ActivityLoginBinding>,
|
||||
private val supportFragmentManager: FragmentManager
|
||||
) : OnboardingVariant {
|
||||
|
||||
private val enterAnim = R.anim.enter_fade_in
|
||||
private val exitAnim = R.anim.exit_fade_out
|
||||
|
||||
private val popEnterAnim = R.anim.no_anim
|
||||
private val popExitAnim = R.anim.exit_fade_out
|
||||
|
||||
private val topFragment: Fragment?
|
||||
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
|
||||
|
||||
private val commonOption: (FragmentTransaction) -> Unit = { ft ->
|
||||
// Find the loginLogo on the current Fragment, this should not return null
|
||||
(topFragment?.view as? ViewGroup)
|
||||
// Find activity.findViewById does not work, I do not know why
|
||||
// activity.findViewById<View?>(views.loginLogo)
|
||||
?.children
|
||||
?.firstOrNull { it.id == R.id.loginLogo }
|
||||
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
}
|
||||
|
||||
override fun initUiAndData(isFirstCreation: Boolean) {
|
||||
if (isFirstCreation) {
|
||||
addFirstFragment()
|
||||
}
|
||||
|
||||
with(activity) {
|
||||
loginViewModel.onEach {
|
||||
updateWithState(it)
|
||||
}
|
||||
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
|
||||
}
|
||||
|
||||
// Get config extra
|
||||
val loginConfig = activity.intent.getParcelableExtra<LoginConfig?>(OnboardingActivity.EXTRA_CONFIG)
|
||||
if (isFirstCreation) {
|
||||
// TODO Check this
|
||||
loginViewModel.handle(LoginAction2.InitWith(loginConfig))
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFirstFragment() {
|
||||
activity.addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java)
|
||||
}
|
||||
|
||||
private fun handleLoginViewEvents(event: LoginViewEvents2) {
|
||||
when (event) {
|
||||
is LoginViewEvents2.RegistrationFlowResult -> {
|
||||
// Check that all flows are supported by the application
|
||||
if (event.flowResult.missingStages.any { !it.isSupported() }) {
|
||||
// Display a popup to propose use web fallback
|
||||
onRegistrationStageNotSupported()
|
||||
} else {
|
||||
if (event.isRegistrationStarted) {
|
||||
// Go on with registration flow
|
||||
handleRegistrationNavigation(event.flowResult)
|
||||
} else {
|
||||
/*
|
||||
// First ask for login and password
|
||||
// I add a tag to indicate that this fragment is a registration stage.
|
||||
// This way it will be automatically popped in when starting the next registration stage
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginFragment2::class.java,
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
is LoginViewEvents2.OutdatedHomeserver -> {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.login_error_outdated_homeserver_title)
|
||||
.setMessage(R.string.login_error_outdated_homeserver_warning_content)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
Unit
|
||||
}
|
||||
is LoginViewEvents2.OpenServerSelection ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginServerSelectionFragment2::class.java,
|
||||
option = { ft ->
|
||||
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
})
|
||||
is LoginViewEvents2.OpenHomeServerUrlFormScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginServerUrlFormFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> {
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginFragmentSigninUsername2::class.java,
|
||||
option = { ft ->
|
||||
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
})
|
||||
}
|
||||
is LoginViewEvents2.OpenSsoOnlyScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginSsoOnlyFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event)
|
||||
is LoginViewEvents2.OpenResetPasswordScreen ->
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginResetPasswordFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
is LoginViewEvents2.OnResetPasswordSendThreePidDone -> {
|
||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginResetPasswordMailConfirmationFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> {
|
||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginResetPasswordSuccessFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone -> {
|
||||
// Go back to the login fragment
|
||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||
}
|
||||
is LoginViewEvents2.OnSendEmailSuccess ->
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginWaitForEmailFragment2::class.java,
|
||||
LoginWaitForEmailFragmentArgument(event.email),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
is LoginViewEvents2.OpenSigninPasswordScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginFragmentSigninPassword2::class.java,
|
||||
tag = FRAGMENT_LOGIN_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OpenSignupPasswordScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginFragmentSignupPassword2::class.java,
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginFragmentSignupUsername2::class.java,
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginFragmentToAny2::class.java,
|
||||
tag = FRAGMENT_LOGIN_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
is LoginViewEvents2.OnSendMsisdnSuccess ->
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment2::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
is LoginViewEvents2.Failure ->
|
||||
// This is handled by the Fragments
|
||||
Unit
|
||||
is LoginViewEvents2.OnLoginModeNotSupported ->
|
||||
onLoginModeNotSupported(event.supportedTypes)
|
||||
is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event)
|
||||
is LoginViewEvents2.Finish -> terminate()
|
||||
is LoginViewEvents2.CancelRegistration -> handleCancelRegistration()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCancelRegistration() {
|
||||
// Cleanup the back stack
|
||||
activity.resetBackstack()
|
||||
}
|
||||
|
||||
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
|
||||
if (event.newAccount) {
|
||||
// Propose to set avatar and display name
|
||||
// Back on this Fragment will finish the Activity
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
AccountCreatedFragment::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
} else {
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun terminate() {
|
||||
val intent = HomeActivity.newIntent(
|
||||
activity,
|
||||
firstStartMainActivity = false,
|
||||
)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
private fun updateWithState(loginViewState2: LoginViewState2) {
|
||||
// Loading
|
||||
setIsLoading(loginViewState2.isLoading)
|
||||
}
|
||||
|
||||
// Hack for AccountCreatedFragment
|
||||
override fun setIsLoading(isLoading: Boolean) {
|
||||
views.loginLoading.isVisible = isLoading
|
||||
}
|
||||
|
||||
private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) {
|
||||
// Pop the backstack
|
||||
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
// And inform the user
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the SSO redirection here.
|
||||
*/
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
intent?.data
|
||||
?.let { tryOrNull { it.getQueryParameter("loginToken") } }
|
||||
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
|
||||
}
|
||||
|
||||
private fun onRegistrationStageNotSupported() {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.app_name)
|
||||
.setMessage(activity.getString(R.string.login_registration_not_supported))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginWebFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.app_name)
|
||||
.setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginWebFragment2::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun handleRegistrationNavigation(flowResult: FlowResult) {
|
||||
// Complete all mandatory stages first
|
||||
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
|
||||
|
||||
if (mandatoryStage != null) {
|
||||
doStage(mandatoryStage)
|
||||
} else {
|
||||
// Consider optional stages
|
||||
val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy }
|
||||
if (optionalStage == null) {
|
||||
// Should not happen...
|
||||
} else {
|
||||
doStage(optionalStage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doStage(stage: Stage) {
|
||||
// Ensure there is no fragment for registration stage in the backstack
|
||||
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
when (stage) {
|
||||
is Stage.ReCaptcha -> activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginCaptchaFragment2::class.java,
|
||||
LoginCaptchaFragmentArgument(stage.publicKey),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
is Stage.Email -> activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment2::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
is Stage.Msisdn -> activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment2::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
is Stage.Terms -> activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
LoginTermsFragment2::class.java,
|
||||
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
else -> Unit // Should not happen
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ import javax.inject.Inject
|
|||
class OnboardingActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedActivity {
|
||||
|
||||
private val onboardingVariant by lifecycleAwareLazy {
|
||||
onboardingVariantFactory.create(this, views = views, onboardingViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel())
|
||||
onboardingVariantFactory.create(this, views = views, onboardingViewModel = lazyViewModel())
|
||||
}
|
||||
|
||||
@Inject lateinit var onboardingVariantFactory: OnboardingVariantFactory
|
||||
|
|
|
@ -20,7 +20,6 @@ import im.vector.app.config.OnboardingVariant
|
|||
import im.vector.app.core.platform.ScreenOrientationLocker
|
||||
import im.vector.app.databinding.ActivityLoginBinding
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.login2.LoginViewModel2
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthVariant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,7 +32,6 @@ class OnboardingVariantFactory @Inject constructor(
|
|||
activity: OnboardingActivity,
|
||||
views: ActivityLoginBinding,
|
||||
onboardingViewModel: Lazy<OnboardingViewModel>,
|
||||
loginViewModel2: Lazy<LoginViewModel2>
|
||||
) = when (vectorFeatures.onboardingVariant()) {
|
||||
OnboardingVariant.LEGACY -> error("Legacy is not supported by the FTUE")
|
||||
OnboardingVariant.FTUE_AUTH -> FtueAuthVariant(
|
||||
|
@ -44,11 +42,5 @@ class OnboardingVariantFactory @Inject constructor(
|
|||
vectorFeatures = vectorFeatures,
|
||||
orientationLocker = orientationLocker
|
||||
)
|
||||
OnboardingVariant.LOGIN_2 -> Login2Variant(
|
||||
views = views,
|
||||
loginViewModel = loginViewModel2.value,
|
||||
activity = activity,
|
||||
supportFragmentManager = activity.supportFragmentManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue