Login UX flow v2

This commit is contained in:
Benoit Marty 2021-04-14 12:35:33 +02:00
parent 344a7e5b3d
commit 408a0fc010
45 changed files with 5536 additions and 3 deletions

View file

@ -6,6 +6,7 @@ Features ✨:
Improvements 🙌:
- Add ability to install APK from directly from Element (#2381)
- Improve login/register flow (#2585, #3172)
Bugfix 🐛:
- Message states cosmetic changes (#3007)

View file

@ -119,6 +119,22 @@
android:scheme="element" />
</intent-filter>
</activity>
<activity
android:name=".features.login2.LoginActivity2"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="connect"
android:scheme="element" />
</intent-filter>
</activity>
<!-- Add tools:ignore="Instantiatable" for the error reported only by Buildkite :/ -->
<activity

View file

@ -28,11 +28,11 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapMigrateBackupFragment
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
import im.vector.app.features.crypto.recover.BootstrapSetupRecoveryKeyFragment
import im.vector.app.features.crypto.recover.BootstrapWaitingFragment
@ -71,6 +71,23 @@ 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.LoginFragment2SigninPassword
import im.vector.app.features.login2.LoginFragment2SigninUsername
import im.vector.app.features.login2.LoginFragment2SignupPassword
import im.vector.app.features.login2.LoginFragment2SignupUsername
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.LoginSignUpSignInSelectionFragment2
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.terms.LoginTermsFragment2
import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
import im.vector.app.features.matrixto.MatrixToUserFragment
import im.vector.app.features.pin.PinFragment
@ -85,11 +102,11 @@ import im.vector.app.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.app.features.roommemberprofile.devices.DeviceListFragment
import im.vector.app.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
import im.vector.app.features.roomprofile.RoomProfileFragment
import im.vector.app.features.roomprofile.alias.RoomAliasFragment
import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.alias.RoomAliasFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
@ -264,6 +281,91 @@ interface FragmentModule {
@FragmentKey(LoginWaitForEmailFragment::class)
fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragment2SigninUsername::class)
fun bindLoginFragment2SigninUsername(fragment: LoginFragment2SigninUsername): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragment2SignupUsername::class)
fun bindLoginFragment2SignupUsername(fragment: LoginFragment2SignupUsername): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragment2SigninPassword::class)
fun bindLoginFragment2SigninPassword(fragment: LoginFragment2SigninPassword): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragment2SignupPassword::class)
fun bindLoginFragment2SignupPassword(fragment: LoginFragment2SignupPassword): 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(LoginSignUpSignInSelectionFragment2::class)
fun bindLoginSignUpSignInSelectionFragment2(fragment: LoginSignUpSignInSelectionFragment2): 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(UserListFragment::class)

View file

@ -53,6 +53,7 @@ import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.link.LinkHandlerActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.VectorAttachmentViewerActivity
@ -132,6 +133,7 @@ interface ScreenComponent {
fun inject(activity: KeysBackupManageActivity)
fun inject(activity: EmojiReactionPickerActivity)
fun inject(activity: LoginActivity)
fun inject(activity: LoginActivity2)
fun inject(activity: LinkHandlerActivity)
fun inject(activity: MainActivity)
fun inject(activity: RoomDirectoryActivity)

View file

@ -24,6 +24,7 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.databinding.FragmentLoginSplashBinding
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
@ -55,6 +56,13 @@ class LoginSplashFragment @Inject constructor(
"Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" +
"Build: ${BuildConfig.BUILD_NUMBER}"
}
views.loginSplashNewFlow.setOnClickListener { startNewFlow() }
}
private fun startNewFlow() {
startActivity(LoginActivity2.newIntent(requireContext(), null))
requireActivity().finish()
}
private fun getStarted() {

View file

@ -0,0 +1,163 @@
/*
* 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.appcompat.app.AlertDialog
import androidx.transition.TransitionInflater
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
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)
sharedElementEnterTransition = TransitionInflater.from(context).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
}.exhaustive
}
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 Failure.Cancelled ->
/* 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
AlertDialog.Builder(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
AlertDialog.Builder(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

@ -0,0 +1,101 @@
/*
* 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.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 = LoginActivity2.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
)
?.let { prefetchUrl(it) }
}
}
}
}

View file

@ -0,0 +1,85 @@
/*
* 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.internal.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 RiotX)
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 ResetLogin : 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()
}

View file

@ -0,0 +1,380 @@
/*
* 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.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
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.airbnb.mvrx.viewModel
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
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.exhaustive
import im.vector.app.core.platform.ToolbarConfigurable
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.LoginWaitForEmailFragmentArgument
import im.vector.app.features.login.isSupported
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.toLocalizedLoginTerms
import im.vector.app.features.login2.terms.LoginTermsFragment2
import im.vector.app.features.pin.UnlockedActivity
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
/**
* The LoginActivity manages the fragment navigation and also display the loading View
*/
open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarConfigurable, UnlockedActivity {
private val loginViewModel: LoginViewModel2 by viewModel()
@Inject lateinit var loginViewModelFactory: LoginViewModel2.Factory
@CallSuper
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
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(R.id.loginFragmentContainer)
private val commonOption: (FragmentTransaction) -> Unit = { ft ->
// Find the loginLogo on the current Fragment, this should not return null
(topFragment?.view as? ViewGroup)
// Find findViewById does not work, I do not know why
// findViewById<View?>(R.id.loginLogo)
?.children
?.firstOrNull { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater)
override fun getCoordinatorLayout() = views.coordinatorLayout
override fun initUiAndData() {
if (isFirstCreation()) {
addFirstFragment()
}
loginViewModel
.subscribe(this) {
updateWithState(it)
}
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (isFirstCreation()) {
// TODO Check this
loginViewModel.handle(LoginAction2.InitWith(loginConfig))
}
}
protected open fun addFirstFragment() {
addFragment(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment2::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
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption
)
*/
}
}
}
is LoginViewEvents2.OutdatedHomeserver -> {
AlertDialog.Builder(this)
.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 ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment2::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})
is LoginViewEvents2.OpenHomeServerUrlFormScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment2::class.java,
option = commonOption)
}
is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment2SigninUsername::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})
}
is LoginViewEvents2.OpenSsoOnlyScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginSsoOnlyFragment2::class.java,
option = commonOption)
}
is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event)
is LoginViewEvents2.OpenResetPasswordScreen ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordFragment2::class.java,
option = commonOption)
is LoginViewEvents2.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordMailConfirmationFragment2::class.java,
option = commonOption)
}
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.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 ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWaitForEmailFragment2::class.java,
LoginWaitForEmailFragmentArgument(event.email),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is LoginViewEvents2.OpenPasswordScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment2SigninPassword::class.java,
option = commonOption)
}
is LoginViewEvents2.OpenSignupPasswordScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment2SignupPassword::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
}
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment2SignupUsername::class.java,
option = commonOption)
}
is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragmentToAny2::class.java,
option = commonOption)
}
is LoginViewEvents2.OnSendMsisdnSuccess ->
addFragmentToBackstack(R.id.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)
}.exhaustive
}
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
// TODO Propose to set avatar and display name
val intent = HomeActivity.newIntent(
this,
accountCreation = event.newAccount
)
startActivity(intent)
finish()
}
private fun updateWithState(LoginViewState2: LoginViewState2) {
// Loading
views.loginLoading.isVisible = LoginViewState2.isLoading
}
private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) {
// Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// And inform the user
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(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?) {
super.onNewIntent(intent)
intent?.data
?.let { tryOrNull { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
}
private fun onRegistrationStageNotSupported() {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)
.setMessage(getString(R.string.login_registration_not_supported))
.setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment2::class.java,
option = commonOption)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)
.setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
.setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(R.id.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 -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginCaptchaFragment2::class.java,
LoginCaptchaFragmentArgument(stage.publicKey),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginTermsFragment2::class.java,
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
else -> Unit // Should not happen
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
companion object {
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
const val VECTOR_REDIRECT_URL = "element://connect"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity2::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig)
}
}
}
}

View file

@ -0,0 +1,194 @@
/*
* 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.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
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.internal.di.MoshiProvider
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
}
AlertDialog.Builder(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())
}
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
@Suppress("DEPRECATION")
super.onReceivedError(view, errorCode, description, failingUrl)
onError(description)
}
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 = MoshiProvider.providesMoshi().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.ResetLogin)
}
override fun updateWithState(state: LoginViewState2) {
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
}

View file

@ -0,0 +1,179 @@
/*
* 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 com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.showPassword
import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.failure.isInvalidPassword
import javax.inject.Inject
/**
* In this screen:
* In signin mode:
* - the user is asked for password to sign in to a homeserver.
* - He also can reset his password
*/
class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragment2<FragmentLogin2SigninPasswordBinding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninPasswordBinding {
return FragmentLogin2SigninPasswordBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupForgottenPasswordButton()
setupPasswordReveal()
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()
)
if (state.loginProfileInfo != null) {
views.loginUserIcon.isVisible = true
Glide.with(requireContext())
.load(state.loginProfileInfo.fullAvatarUrl)
.apply(RequestOptions.circleCropTransform())
.into(views.loginUserIcon)
} else {
views.loginUserIcon.isVisible = false
}
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
views.passwordField
.textChanges()
.map { it.isNotEmpty() }
.subscribeBy {
views.passwordFieldTil.error = null
views.loginSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
private fun forgetPasswordClicked() {
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
}
private fun setupPasswordReveal() {
passwordShown = false
views.passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
views.passwordField.showPassword(passwordShown)
views.passwordReveal.render(passwordShown)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
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
passwordShown = false
renderPasswordField()
}
}
/**
* Detect if password ends or starts with spaces
*/
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
}

View file

@ -0,0 +1,107 @@
/*
* 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 com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.databinding.FragmentLogin2SigninUsernameBinding
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
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 LoginFragment2SigninUsername @Inject constructor() : AbstractLoginFragment2<FragmentLogin2SigninUsernameBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninUsernameBinding {
return FragmentLogin2SigninUsernameBinding.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() }
.subscribeBy {
views.loginFieldTil.error = null
views.loginSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
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

@ -0,0 +1,148 @@
/*
* 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 com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.showPassword
import im.vector.app.databinding.FragmentLogin2SignupPasswordBinding
import io.reactivex.rxkotlin.Observables
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
/**
* In this screen:
* - the user is asked for password to sign up to a homeserver.
*/
class LoginFragment2SignupPassword @Inject constructor() : AbstractSSOLoginFragment2<FragmentLogin2SignupPasswordBinding>() {
private var passwordsShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupPasswordBinding {
return FragmentLogin2SignupPasswordBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupAutoFill()
setupPasswordReveal()
views.passwordFieldRepeat.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)
views.passwordFieldRepeat.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++
}
val passwordRepeat = views.passwordFieldRepeat.text.toString()
if (password != passwordRepeat) {
views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match)
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() }
Observables.combineLatest(
views.passwordField.textChanges(),
views.passwordFieldRepeat.textChanges()
)
.subscribeBy { (password, passwordRepeat) ->
views.passwordFieldTil.error = null
views.passwordFieldTilRepeat.error = null
views.loginSubmit.isEnabled = password.isNotEmpty() && passwordRepeat.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupPasswordReveal() {
passwordsShown = false
views.passwordReveal.setOnClickListener {
passwordsShown = !passwordsShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
views.passwordReveal.render(passwordsShown)
views.passwordField.showPassword(passwordsShown)
views.passwordFieldRepeat.showPassword(passwordsShown)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
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 passwords are hidden
passwordsShown = false
renderPasswordField()
}
}
}

View file

@ -0,0 +1,141 @@
/*
* 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.text.isDigitsOnly
import androidx.core.view.isVisible
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLogin2SignupUsernameBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SocialLoginButtonsView
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
/**
* In this screen:
* - the user is asked for login to sign up to a homeserver.
* - SSO option are displayed if available
*/
class LoginFragment2SignupUsername @Inject constructor() : AbstractSSOLoginFragment2<FragmentLogin2SignupUsernameBinding>() {
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
// waiting for https://github.com/matrix-org/synapse/issues/7576
private var isNumericOnlyUserIdForbidden = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupUsernameBinding {
return FragmentLogin2SignupUsernameBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupAutoFill()
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
}
}
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 (isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = "The homeserver does not accept username with only digits."
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
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
)
?.let { openInCustomTab(it) }
}
}
} else {
views.loginSocialLoginContainer.isVisible = false
views.loginSocialLoginButtons.ssoIdentityProviders = null
}
}
@SuppressLint("SetTextI18n")
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
views.loginField.textChanges()
.map { it.trim() }
.subscribeBy { text ->
val isNotEmpty = text.isNotEmpty()
views.loginFieldTil.error = null
views.loginSubmit.isEnabled = isNotEmpty
}
.disposeOnDestroyView()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
override fun onError(throwable: Throwable) {
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
@SuppressLint("SetTextI18n")
override fun updateWithState(state: LoginViewState2) {
isNumericOnlyUserIdForbidden = state.isNumericOnlyUserIdForbidden
setupUi(state)
}
}

View file

@ -0,0 +1,227 @@
/*
* 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.text.isDigitsOnly
import androidx.core.view.isVisible
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLogin2SigninToBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SocialLoginButtonsView
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
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 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<FragmentLogin2SigninToBinding>() {
private var passwordShown = false
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
// waiting for https://github.com/matrix-org/synapse/issues/7576
private var isNumericOnlyUserIdForbidden = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninToBinding {
return FragmentLogin2SigninToBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupForgottenPasswordButton()
setupPasswordReveal()
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.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
}
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 (isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = "The homeserver does not accept username with only digits."
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
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
)
?.let { openInCustomTab(it) }
}
}
} else {
views.loginSocialLoginContainer.isVisible = false
views.loginSocialLoginButtons.ssoIdentityProviders = null
}
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
Observable
.combineLatest(
views.loginField.textChanges().map { it.trim().isNotEmpty() },
views.passwordField.textChanges().map { it.isNotEmpty() },
{ isLoginNotEmpty, isPasswordNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty
}
)
.subscribeBy {
views.loginFieldTil.error = null
views.passwordFieldTil.error = null
views.loginSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
private fun forgetPasswordClicked() {
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
}
private fun setupPasswordReveal() {
passwordShown = false
views.passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
views.passwordField.showPassword(passwordShown)
views.passwordReveal.render(passwordShown)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
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) {
isNumericOnlyUserIdForbidden = state.isNumericOnlyUserIdForbidden
setupUi(state)
if (state.isLoading) {
// Ensure password is hidden
passwordShown = false
renderPasswordField()
}
}
/**
* Detect if password ends or starts with spaces
*/
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
}

View file

@ -0,0 +1,258 @@
/*
* 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.os.Parcelable
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 com.airbnb.mvrx.args
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.jakewharton.rxbinding3.widget.textChanges
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.databinding.FragmentLoginGenericTextInputFormBinding
import kotlinx.parcelize.Parcelize
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 javax.inject.Inject
enum class TextInputFormFragmentMode {
SetEmail,
SetMsisdn,
ConfirmMsisdn
}
@Parcelize
data class LoginGenericTextInputFormFragmentArgument(
val mode: TextInputFormFragmentMode,
val mandatory: Boolean,
val extra: String = ""
) : Parcelable
/**
* In this screen, the user is asked for a text input
*/
class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginGenericTextInputFormBinding>() {
private val params: LoginGenericTextInputFormFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputFormBinding {
return FragmentLoginGenericTextInputFormBinding.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() }
}
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()
.subscribe {
views.loginGenericTextInputFormTil.error = null
}
.disposeOnDestroyView()
}
private fun setupUi() {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title)
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice)
views.loginGenericTextInputFormNotice2.setTextOrHide(null)
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)
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice)
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.loginGenericTextInputFormNotice2.setTextOrHide(null)
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()
.subscribe {
views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(it)
}
.disposeOnDestroyView()
}
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.ResetLogin)
}
}

View file

@ -0,0 +1,172 @@
/*
* 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.appcompat.app.AlertDialog
import androidx.autofill.HintConstants
import com.jakewharton.rxbinding3.widget.textChanges
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.showPassword
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginResetPassword2Binding
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
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>() {
private var passwordsShown = false
// 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()
setupPasswordReveal()
setupAutoFill()
}
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)
views.passwordFieldRepeat.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() }
Observable
.combineLatest(
views.resetPasswordEmail.textChanges().map { it.isEmail() },
views.passwordField.textChanges().map { it.isNotEmpty() },
{ isEmail, isPasswordNotEmpty ->
isEmail && isPasswordNotEmpty
}
)
.subscribeBy {
views.resetPasswordEmailTil.error = null
views.passwordFieldTil.error = null
views.resetPasswordSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
private fun submit() {
cleanupUi()
var error = 0
val password = views.passwordField.text.toString()
val passwordRepeat = views.passwordFieldRepeat.text.toString()
if (password != passwordRepeat) {
views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match)
error++
}
if (error > 0) {
return
}
if (showWarning) {
showWarning = false
// Display a warning as Riot-Web does first
AlertDialog.Builder(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) { _, _ ->
doSubmit()
}
.setNegativeButton(R.string.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
views.passwordFieldTilRepeat.error = null
}
private fun setupPasswordReveal() {
passwordsShown = false
views.passwordReveal.setOnClickListener {
passwordsShown = !passwordsShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
views.passwordField.showPassword(passwordsShown)
views.passwordFieldRepeat.showPassword(passwordsShown)
views.passwordReveal.render(passwordsShown)
}
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 passwords are hidden
passwordsShown = false
renderPasswordField()
}
}
}

View file

@ -0,0 +1,75 @@
/*
* 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 androidx.appcompat.app.AlertDialog
import im.vector.app.R
import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding
import org.matrix.android.sdk.api.failure.is401
import javax.inject.Inject
/**
* In this screen, the user is asked to check his email and to click on a button once it's done
*/
class LoginResetPasswordMailConfirmationFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPasswordMailConfirmationBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmationBinding {
return FragmentLoginResetPasswordMailConfirmationBinding.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)
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
}
}

View file

@ -0,0 +1,49 @@
/*
* 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

@ -0,0 +1,75 @@
/*
* 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 im.vector.app.databinding.FragmentLoginServerSelection2Binding
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() }
}
@SuppressLint("SetTextI18n")
private fun updateUi(state: LoginViewState2) {
when (state.signMode) {
SignMode2.Unknown -> Unit
SignMode2.SignUp -> {
views.loginServerTitle.text = "Please choose a server"
}
SignMode2.SignIn -> {
views.loginServerTitle.text = "Please choose your server"
}
}
}
private fun selectMatrixOrg() {
loginViewModel.handle(LoginAction2.ChooseDefaultHomeServer)
}
private fun selectOther() {
loginViewModel.handle(LoginAction2.EnterServerUrl)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
}
override fun updateWithState(state: LoginViewState2) {
updateUi(state)
}
}

View file

@ -0,0 +1,143 @@
/*
* 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 com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
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() : 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()
.subscribe {
views.loginServerUrlFormHomeServerUrlTil.error = null
views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
}
.disposeOnDestroyView()
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 (BuildConfig.DEBUG) 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

@ -0,0 +1,72 @@
/*
* 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.BuildConfig
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 LoginSignUpSignInSelectionFragment2 @Inject constructor(
private val vectorPreferences: VectorPreferences
) : 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 (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
views.loginSplashVersion.isVisible = true
@SuppressLint("SetTextI18n")
views.loginSplashVersion.text = "Version : ${BuildConfig.VERSION_NAME}\n" +
"Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" +
"Build: ${BuildConfig.BUILD_NUMBER}"
}
}
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

@ -0,0 +1,68 @@
/*
* 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 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 = LoginActivity2.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

@ -0,0 +1,59 @@
/*
* 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 OpenPasswordScreen : 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()
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()
}

View file

@ -0,0 +1,828 @@
/*
* 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 androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
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.extensions.configureAndStart
import im.vector.app.core.extensions.exhaustive
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.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.LoginFlowResult
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 {
fun create(initialState: LoginViewState2): LoginViewModel2
}
init {
getKnownCustomHomeServersUrls()
}
private fun getKnownCustomHomeServersUrls() {
setState {
copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls())
}
}
companion object : MvRxViewModelFactory<LoginViewModel2, LoginViewState2> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LoginViewState2): LoginViewModel2? {
return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) {
is LoginActivity2 -> activity.loginViewModelFactory.create(state)
// TODO is SoftLogoutActivity -> activity.loginViewModelFactory.create(state)
else -> error("Invalid Activity")
}
}
}
// 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?.currentThreePid
// 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)
}.exhaustive
}
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)
}
}
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,
isNumericOnlyUserIdForbidden = action.homeServerUrl == matrixOrgUrl,
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.ResetLogin -> {
viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration()
setState { copy(isLoading = false) }
}
}
LoginAction2.ResetResetPassword -> {
setState {
copy(
resetPasswordEmail = null
)
}
}
}
}
private fun handleUpdateSignMode(action: LoginAction2.UpdateSignMode) {
setState {
// Always create a new state, to ensure the state is correctly reset
LoginViewState2(
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, action.newPassword)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
return@launch
}
setState {
copy(
isLoading = false,
resetPasswordEmail = action.email
)
}
_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 {
safeLoginWizard.resetPasswordMailConfirmed()
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
return@launch
}
setState {
copy(
isLoading = false,
resetPasswordEmail = 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)
}.exhaustive
}
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)
}.exhaustive
}
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,
deviceName = 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 home server is valid
if (data.homeServerUrl != null && data.wellKnown != null) {
onWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig)
} else {
onWellKnownError()
}
is WellknownResult.InvalidMatrixId -> {
setState { copy(isLoading = false) }
_viewEvents.post(LoginViewEvents2.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id))))
}
else -> {
onWellKnownError()
}
}.exhaustive
}
}
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(
homeServerUri = 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
}
if (data is LoginFlowResult.Success) {
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.OpenPasswordScreen
}
is LoginMode.Sso -> {
LoginViewEvents2.OpenSsoOnlyScreen
}
LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList())
LoginMode.Unknown -> null
}
viewEvent?.let { _viewEvents.post(it) }
val urlFromUser = action.username.substringAfter(":")
setState {
copy(
isLoading = false,
homeServerUrlFromUser = urlFromUser,
homeServerUrl = data.homeServerUrl,
isNumericOnlyUserIdForbidden = urlFromUser == matrixOrgUrl,
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) {
try {
val info = safeLoginWizard.getProfileInfo(username)
setState {
copy(
loginProfileInfo = info
)
}
} catch (failure: Throwable) {
// Ignore error
// TODO 404 may indicates that the user does not exist, so there is a mistake in the id
}
}
}
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
}
if (data is LoginFlowResult.Success) {
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, data.homeServerUrl 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,
isNumericOnlyUserIdForbidden = homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl,
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

@ -0,0 +1,69 @@
/*
* 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.MvRxState
import com.airbnb.mvrx.PersistState
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 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: LoginProfileInfo? = null,
// True on Matrix.org
val isNumericOnlyUserIdForbidden: Boolean = false,
// Network result
@PersistState
val loginMode: LoginMode = LoginMode.Unknown,
// From database
val knownCustomHomeServersUrls: List<String> = emptyList()
) : MvRxState {
// Pending user identifier
fun userIdentifier(): String {
return if (userName != null && MatrixPatterns.isUserId(userName)) {
userName
} else {
"@$userName:${homeServerUrlFromUser.toReducedUrl()}"
}
}
}

View file

@ -0,0 +1,75 @@
/*
* 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 his 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.ResetLogin)
}
}

View file

@ -0,0 +1,255 @@
/*
* 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 androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
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.internal.di.MoshiProvider
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 var isWebViewLoaded = false
private var isForSessionRecovery = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.loginWebToolbar)
}
override fun updateWithState(state: LoginViewState2) {
setupTitle(state)
isForSessionRecovery = state.deviceId?.isNotBlank() == true
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
private fun setupTitle(state: LoginViewState2) {
views.loginWebToolbar.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) {
AlertDialog.Builder(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()
}
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)
views.loginWebToolbar.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
*/
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 = MoshiProvider.providesMoshi().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) {
val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
} else {
loginViewModel.handle(LoginAction2.WebLoginSuccess(credentials))
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetLogin)
}
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

@ -0,0 +1,27 @@
/*
* 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

@ -0,0 +1,119 @@
/*
* 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.FragmentLoginTermsBinding
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.internal.auth.registration.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<FragmentLoginTermsBinding>(),
PolicyController.PolicyControllerListener {
private val params: LoginTermsFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTermsBinding {
return FragmentLoginTermsBinding.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.ResetLogin)
}
}

View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/loginUserIcon"
android:layout_width="92dp"
android:layout_height="92dp"
android:importantForAccessibility="no"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/loginWelcomeBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="Welcome back user!" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_please_enter_your_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<im.vector.app.core.ui.views.RevealPasswordImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:tint="?attr/colorAccent" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin">
<com.google.android.material.button.MaterialButton
android:id="@+id/forgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/auth_forgot_password" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/auth_login"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
tools:text="@string/login_signin_to" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/login_signin_username_hint"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<im.vector.app.core.ui.views.RevealPasswordImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:tint="?attr/colorAccent" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="22dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/forgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/auth_forgot_password" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/auth_login"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</FrameLayout>
<!-- Social Logins buttons -->
<LinearLayout
android:id="@+id/loginSocialLoginContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/loginSocialLoginHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/login_social_continue"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
android:textSize="14sp" />
<im.vector.app.features.login.SocialLoginButtonsView
android:id="@+id/loginSocialLoginButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:signMode="signin" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_please_enter_your_matrix_identifier"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_please_enter_your_matrix_identifier_help"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/login_signin_matrix_id_hint"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/_continue"
tools:enabled="false" />
<TextView
android:id="@+id/loginServerText3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="start"
android:text="@string/login_enter_identifier_help"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginChooseAServer"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/login_choose_a_server" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_please_choose_a_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_your_matrix_identifier"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<TextView
android:id="@+id/loginMatrixIdentifier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="\@user:domain.org" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_press_back_to_change"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textStyle="italic" />
<FrameLayout
android:id="@+id/passwordLabelContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/login_choose_a_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<im.vector.app.core.ui.views.RevealPasswordImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:tint="?attr/colorAccent" />
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionNext"
android:inputType="textPassword"
android:maxLines="1"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTilRepeat"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/login_signup_password_repeat_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordFieldRepeat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/_continue"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_please_choose_a_user_name"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="@string/login_signup_to" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/login_signup_username_hint"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/loginChooseHelp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_please_choose_a_user_name_help_2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textStyle="italic" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:text="@string/_continue"
tools:enabled="false" />
<!-- SSO Option -->
<!-- Social Logins buttons -->
<LinearLayout
android:id="@+id/loginSocialLoginContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/loginSocialLoginHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/login_social_continue"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
android:textSize="14sp" />
<im.vector.app.features.login.SocialLoginButtonsView
android:id="@+id/loginSocialLoginButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:signMode="signin" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/resetPasswordTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
tools:text="@string/login_reset_password_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="32dp"
android:text="@string/login_enter_your_email"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/resetPasswordEmailTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/login_reset_password_email_hint"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/resetPasswordEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/loginNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:text="@string/login_reset_password_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" />
<FrameLayout
android:id="@+id/passwordLabelContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/login_choose_a_new_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<im.vector.app.core.ui.views.RevealPasswordImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:tint="?attr/colorAccent" />
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/login_reset_password_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword"
android:maxLines="1"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTilRepeat"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/login_signup_password_repeat_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordFieldRepeat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/resetPasswordSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/login_reset_password_submit"
tools:ignore="RelativeOverlap" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_reset_password_success_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/resetPasswordSuccessNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_reset_password_success_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_reset_password_success_notice_2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" />
<com.google.android.material.button.MaterialButton
android:id="@+id/resetPasswordSuccessSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_reset_password_success_submit" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginServerTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
tools:ignore="UnknownId"
tools:text="Please choose a server" />
<TextView
android:id="@+id/loginServerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="start"
android:text="@string/login_server_text"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<!-- Use a CheckableConstraintLayout to keep the pressed state when retrieving login flow -->
<im.vector.app.core.platform.CheckableConstraintLayout
android:id="@+id/loginServerChoiceMatrixOrg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@drawable/bg_login_server_selector"
android:contentDescription="@string/login_a11y_choose_matrix_org"
android:minHeight="80dp"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin">
<ImageView
android:id="@+id/loginServerChoiceMatrixOrgIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_logo_matrix_org"
app:layout_constraintBottom_toTopOf="@+id/loginServerChoiceMatrixOrgText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/loginServerChoiceMatrixOrgText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="start"
android:text="@string/login_server_matrix_org_text"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceMatrixOrgIcon" />
</im.vector.app.core.platform.CheckableConstraintLayout>
<TextView
android:id="@+id/loginServerText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/login_if_you_re_not_sure_select_this_option"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textStyle="italic" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginServerChoiceOther"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:background="@drawable/bg_login_server_selector"
android:contentDescription="@string/login_a11y_choose_other"
android:minHeight="80dp"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin">
<TextView
android:id="@+id/loginServerChoiceOtherTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/login_element_matrix_server_and_others"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/loginServerChoiceOtherText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/loginServerChoiceOtherText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="start"
android:text="@string/login_server_other_text"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOtherTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginServerUrlFormTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
android:text="@string/login_server_url_form_common_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginServerUrlFormHomeServerUrlTil"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
app:errorEnabled="true"
tools:hint="@string/login_server_url_form_modular_hint">
<AutoCompleteTextView
android:id="@+id/loginServerUrlFormHomeServerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hs_url"
android:imeOptions="actionDone"
android:inputType="textUri"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/loginServerUrlFormClearHistory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/login_clear_homeserver_history"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textColor="@color/riotx_accent"
android:visibility="invisible"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginServerUrlFormSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="22dp"
android:text="@string/login_continue" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -204,4 +204,13 @@
tools:text="@string/settings_version"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSplashNewFlow"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_try_the_new_flow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background"
android:paddingStart="36dp"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="36dp"
android:paddingBottom="@dimen/layout_vertical_margin">
<!-- Strategy: Spaces are used to spread the remaining space, using weight -->
<Space
android:id="@+id/loginSplashSpace1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashLogoContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintVertical_weight="4" />
<LinearLayout
android:id="@+id/loginSplashLogoContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace1">
<ImageView
android:id="@+id/loginSplashLogo"
android:layout_width="64dp"
android:layout_height="64dp"
android:importantForAccessibility="no"
android:src="@drawable/element_logo_green"
android:transitionName="loginLogoTransition" />
<ImageView
android:id="@+id/logoType"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginTop="8dp"
android:contentDescription="@string/app_name"
android:src="@drawable/element_logotype"
app:tint="?colorAccent"
tools:ignore="MissingPrefix" />
</LinearLayout>
<Space
android:id="@+id/loginSplashSpace2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle"
app:layout_constraintTop_toBottomOf="@+id/loginSplashLogoContainer"
app:layout_constraintVertical_weight="1" />
<TextView
android:id="@+id/loginSplashTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/login_splash_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
android:transitionName="loginTitleTransition"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace25"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace2" />
<Space
android:id="@+id/loginSplashSpace25"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashContent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle"
app:layout_constraintVertical_weight="3" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginSplashContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace25">
<ImageView
android:id="@+id/loginSplashPicto1"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_message_circle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginSplashText1"
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/loginSplashText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="start"
android:text="@string/login_splash_text1"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/loginSplashPicto2"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_lock"
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText2"
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/loginSplashText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
<ImageView
android:id="@+id/loginSplashPicto3"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_sliders"
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText3"
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/loginSplashText3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text3"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Space
android:id="@+id/loginSplashSpace3"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginFormContent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashContent"
app:layout_constraintVertical_weight="3" />
<LinearLayout
android:id="@+id/loginFormContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace3">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSignupSigninSignUp"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/login_create_a_new_account" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSignupSigninSignIn"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="14dp"
android:text="@string/login_i_already_have_an_account" />
</LinearLayout>
<Space
android:id="@+id/loginSplashSpace5"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormContent"
app:layout_constraintVertical_weight="6" />
<TextView
android:id="@+id/loginSplashVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?riotx_text_secondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/settings_version"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<androidx.constraintlayout.widget.ConstraintLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginSignupSigninTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="56dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginLogo"
tools:ignore="UnknownId"
tools:text="@string/login_connect_to" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSignupSigninSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:text="@string/login_signin_sso"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/loginWaitForEmailTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_wait_for_email_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginWaitForEmailNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="@string/login_wait_for_email_notice_2" />
<TextView
android:id="@+id/loginWaitForEmailHelp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="start"
android:text="@string/login_wait_for_email_help"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" />
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:indeterminate="true" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -58,6 +58,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/auth_forgot_password" />
<com.google.android.material.button.MaterialButton

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Those strings are not final, so do not put them into Weblate for the moment -->
<string name="login_signup_password_repeat_hint">Type it again</string>
<string name="login_welcome_back">Welcome back %s!</string>
<string name="login_please_enter_your_password">Please enter your password</string>
<string name="login_please_enter_your_matrix_identifier">Please enter your Matrix identifier</string>
<string name="login_please_enter_your_matrix_identifier_help">Matrix identifiers start with @, for instance @alice:server.org</string>
<string name="login_enter_identifier_help">If you do not know your Matrix identifier, or if your account has been created using Single Sign On (for instance using a Google account), or if you want to connect using your simple name, or an email associated to your account, you have to select your server first.</string>
<string name="login_choose_a_server">Choose a server</string>
<string name="login_please_choose_a_password">Please choose a password</string>
<string name="login_your_matrix_identifier">Your Matrix identifier</string>
<string name="login_press_back_to_change">Press back to change</string>
<string name="login_choose_a_password">Choose a password</string>
<string name="login_enter_your_email">Enter an email associated to your Matrix account</string>
<string name="login_choose_a_new_password">Choose a new password</string>
<string name="login_please_choose_a_user_name">Please choose an identifier</string>
<string name="login_please_choose_a_user_name_help">Your identifier will be used to connect to your Matrix account</string>
<string name="login_please_choose_a_user_name_help_2">Once your account is created, your identifier cannot be modified. However you will be able to change your display name.</string>
<string name="login_if_you_re_not_sure_select_this_option">If you\'re not sure, select this option</string>
<string name="login_element_matrix_server_and_others">Element Matrix Server and others</string>
<string name="login_try_the_new_flow">Try the new flow</string>
<string name="login_create_a_new_account">Create a new account</string>
<string name="login_i_already_have_an_account">I already have an account</string>
<string name="login_wait_for_email_notice_2">We just sent an email to %1$s.</string>
<string name="login_wait_for_email_help">Click on the link it contains to continue the account creation.</string>
</resources>