mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-27 20:09:27 +03:00
Login UX flow v2
This commit is contained in:
parent
344a7e5b3d
commit
408a0fc010
45 changed files with 5536 additions and 3 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
119
vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt
Executable file
119
vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt
Executable 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)
|
||||
}
|
||||
}
|
113
vector/src/main/res/layout/fragment_login_2_signin_password.xml
Normal file
113
vector/src/main/res/layout/fragment_login_2_signin_password.xml
Normal 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>
|
||||
|
144
vector/src/main/res/layout/fragment_login_2_signin_to.xml
Normal file
144
vector/src/main/res/layout/fragment_login_2_signin_to.xml
Normal 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>
|
||||
|
|
@ -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>
|
132
vector/src/main/res/layout/fragment_login_2_signup_password.xml
Normal file
132
vector/src/main/res/layout/fragment_login_2_signup_password.xml
Normal 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>
|
||||
|
103
vector/src/main/res/layout/fragment_login_2_signup_username.xml
Normal file
103
vector/src/main/res/layout/fragment_login_2_signup_username.xml
Normal 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>
|
140
vector/src/main/res/layout/fragment_login_reset_password_2.xml
Normal file
140
vector/src/main/res/layout/fragment_login_reset_password_2.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
133
vector/src/main/res/layout/fragment_login_server_selection_2.xml
Normal file
133
vector/src/main/res/layout/fragment_login_server_selection_2.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
224
vector/src/main/res/layout/fragment_login_splash_2.xml
Normal file
224
vector/src/main/res/layout/fragment_login_splash_2.xml
Normal 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>
|
46
vector/src/main/res/layout/fragment_login_sso_only_2.xml
Normal file
46
vector/src/main/res/layout/fragment_login_sso_only_2.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
31
vector/src/main/res/values/strings_login_v2.xml
Normal file
31
vector/src/main/res/values/strings_login_v2.xml
Normal 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>
|
Loading…
Reference in a new issue