removing login2 classes

This commit is contained in:
Adam Brown 2022-08-11 16:41:49 +01:00
parent 62f7b40a3e
commit 19261ab2d0
37 changed files with 12 additions and 4598 deletions

View file

@ -18,6 +18,5 @@ package im.vector.app.config
enum class OnboardingVariant {
LEGACY,
LOGIN_2,
FTUE_AUTH
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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