Merge pull request #3188 from vector-im/feature/bma/login_v2

Login v2 - WIP
This commit is contained in:
Benoit Marty 2021-05-21 14:55:22 +02:00 committed by GitHub
commit 6706a88a21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 6679 additions and 26 deletions

View File

@ -48,7 +48,7 @@ data class SsoIdentityProvider(
*/
@Json(name = "brand") val brand: String?
) : Parcelable {
) : Parcelable, Comparable<SsoIdentityProvider> {
companion object {
const val BRAND_GOOGLE = "org.matrix.google"
@ -58,4 +58,25 @@ data class SsoIdentityProvider(
const val BRAND_TWITTER = "org.matrix.twitter"
const val BRAND_GITLAB = "org.matrix.gitlab"
}
override fun compareTo(other: SsoIdentityProvider): Int {
return other.toPriority().compareTo(toPriority())
}
private fun toPriority(): Int {
return when (brand) {
// We are on Android, so user is more likely to have a Google account
BRAND_GOOGLE -> 5
// Facebook is also an important SSO provider
BRAND_FACEBOOK -> 4
// Twitter is more for professionals
BRAND_TWITTER -> 3
// Here it's very for techie people
BRAND_GITHUB,
BRAND_GITLAB -> 2
// And finally, if the account has been created with an iPhone...
BRAND_APPLE -> 1
else -> 0
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth.login
data class LoginProfileInfo(
val matrixId: String,
val displayName: String?,
val fullAvatarUrl: String?
)

View File

@ -24,6 +24,11 @@ import org.matrix.android.sdk.api.session.Session
* More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md
*/
interface LoginWizard {
/**
* Get some information about a matrixId: displayName and avatar url
*/
suspend fun getProfileInfo(matrixId: String): LoginProfileInfo
/**
* Login to the homeserver.
*

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.Availability
import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse
import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams
@ -73,6 +74,15 @@ internal interface AuthAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available")
suspend fun registerAvailable(@Query("username") username: String): Availability
/**
* Get the combined profile information for this user.
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
* This API may return keys which are not limited to displayname or avatar_url.
* @param userId the user id to fetch profile info
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}")
suspend fun getProfile(@Path("userId") userId: String): JsonDict
/**
* Add 3Pid during registration
* Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth.login
import android.util.Patterns
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.session.Session
@ -30,6 +31,7 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams
import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
@ -39,6 +41,15 @@ internal class DefaultLoginWizard(
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI,
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
)
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
return getProfileTask.execute(GetProfileTask.Params(matrixId))
}
override suspend fun login(login: String,
password: String,
deviceName: String): Session {

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.auth.login
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.internal.auth.AuthAPI
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
internal interface GetProfileTask : Task<GetProfileTask.Params, LoginProfileInfo> {
data class Params(
val userId: String
)
}
internal class DefaultGetProfileTask(
private val authAPI: AuthAPI,
private val contentUrlResolver: ContentUrlResolver
) : GetProfileTask {
override suspend fun execute(params: GetProfileTask.Params): LoginProfileInfo {
val info = executeRequest(null) {
authAPI.getProfile(params.userId)
}
return LoginProfileInfo(
matrixId = params.userId,
displayName = info[ProfileService.DISPLAY_NAME_KEY] as? String,
fullAvatarUrl = contentUrlResolver.resolveFullSize(info[ProfileService.AVATAR_URL_KEY] as? String)
)
}
}

View File

@ -137,6 +137,11 @@ android {
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
resValue "string", "build_number", "\"${buildNumber}\""
// The two booleans must not have the same value. We need two values for the manifest
// LoginFlowV2 is disabled to be merged on develop (changelog: Improve login/register flow (#1410, #2585, #3172))
resValue "bool", "useLoginV1", "true"
resValue "bool", "useLoginV2", "false"
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@ -106,6 +106,24 @@
<activity
android:name=".features.login.LoginActivity"
android:launchMode="singleTask"
android:enabled="@bool/useLoginV1"
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>
<activity
android:name=".features.login2.LoginActivity2"
android:launchMode="singleTask"
android:enabled="@bool/useLoginV2"
android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter>

View File

@ -28,11 +28,11 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapMigrateBackupFragment
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
import im.vector.app.features.crypto.recover.BootstrapSetupRecoveryKeyFragment
import im.vector.app.features.crypto.recover.BootstrapWaitingFragment
@ -71,6 +71,24 @@ import im.vector.app.features.login.LoginSplashFragment
import im.vector.app.features.login.LoginWaitForEmailFragment
import im.vector.app.features.login.LoginWebFragment
import im.vector.app.features.login.terms.LoginTermsFragment
import im.vector.app.features.login2.LoginCaptchaFragment2
import im.vector.app.features.login2.LoginFragmentSigninPassword2
import im.vector.app.features.login2.LoginFragmentSigninUsername2
import im.vector.app.features.login2.LoginFragmentSignupPassword2
import im.vector.app.features.login2.LoginFragmentSignupUsername2
import im.vector.app.features.login2.created.AccountCreatedFragment
import im.vector.app.features.login2.LoginFragmentToAny2
import im.vector.app.features.login2.LoginGenericTextInputFormFragment2
import im.vector.app.features.login2.LoginResetPasswordFragment2
import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2
import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2
import im.vector.app.features.login2.LoginServerSelectionFragment2
import im.vector.app.features.login2.LoginServerUrlFormFragment2
import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2
import im.vector.app.features.login2.LoginSsoOnlyFragment2
import im.vector.app.features.login2.LoginWaitForEmailFragment2
import im.vector.app.features.login2.LoginWebFragment2
import im.vector.app.features.login2.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 +103,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
@ -267,6 +285,96 @@ interface FragmentModule {
@FragmentKey(LoginWaitForEmailFragment::class)
fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragmentSigninUsername2::class)
fun bindLoginFragmentSigninUsername2(fragment: LoginFragmentSigninUsername2): Fragment
@Binds
@IntoMap
@FragmentKey(AccountCreatedFragment::class)
fun bindAccountCreatedFragment(fragment: AccountCreatedFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragmentSignupUsername2::class)
fun bindLoginFragmentSignupUsername2(fragment: LoginFragmentSignupUsername2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragmentSigninPassword2::class)
fun bindLoginFragmentSigninPassword2(fragment: LoginFragmentSigninPassword2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragmentSignupPassword2::class)
fun bindLoginFragmentSignupPassword2(fragment: LoginFragmentSignupPassword2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginCaptchaFragment2::class)
fun bindLoginCaptchaFragment2(fragment: LoginCaptchaFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginFragmentToAny2::class)
fun bindLoginFragmentToAny2(fragment: LoginFragmentToAny2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginTermsFragment2::class)
fun bindLoginTermsFragment2(fragment: LoginTermsFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginServerUrlFormFragment2::class)
fun bindLoginServerUrlFormFragment2(fragment: LoginServerUrlFormFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginResetPasswordMailConfirmationFragment2::class)
fun bindLoginResetPasswordMailConfirmationFragment2(fragment: LoginResetPasswordMailConfirmationFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginResetPasswordFragment2::class)
fun bindLoginResetPasswordFragment2(fragment: LoginResetPasswordFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginResetPasswordSuccessFragment2::class)
fun bindLoginResetPasswordSuccessFragment2(fragment: LoginResetPasswordSuccessFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginServerSelectionFragment2::class)
fun bindLoginServerSelectionFragment2(fragment: LoginServerSelectionFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSsoOnlyFragment2::class)
fun bindLoginSsoOnlyFragment2(fragment: LoginSsoOnlyFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSplashSignUpSignInSelectionFragment2::class)
fun bindLoginSplashSignUpSignInSelectionFragment2(fragment: LoginSplashSignUpSignInSelectionFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginWebFragment2::class)
fun bindLoginWebFragment2(fragment: LoginWebFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginGenericTextInputFormFragment2::class)
fun bindLoginGenericTextInputFormFragment2(fragment: LoginGenericTextInputFormFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(LoginWaitForEmailFragment2::class)
fun bindLoginWaitForEmailFragment2(fragment: LoginWaitForEmailFragment2): Fragment
@Binds
@IntoMap
@FragmentKey(UserListFragment::class)

View File

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

View File

@ -94,6 +94,12 @@ fun <T : Fragment> AppCompatActivity.addFragmentToBackstack(
}
}
fun AppCompatActivity.resetBackstack() {
repeat(supportFragmentManager.backStackEntryCount) {
supportFragmentManager.popBackStack()
}
}
fun AppCompatActivity.hideKeyboard() {
currentFocus?.hideKeyboard()
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.extensions
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
/**
* It maybe already exist somewhere but I cannot find it
*/
suspend fun <T> tryAsync(block: suspend () -> T): Async<T> {
return try {
Success(block.invoke())
} catch (failure: Throwable) {
Fail(failure)
}
}

View File

@ -62,7 +62,7 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
@StringRes coloredTextRes: Int,
@AttrRes colorAttribute: Int = R.attr.colorAccent,
underline: Boolean = false,
onClick: (() -> Unit)?) {
onClick: (() -> Unit)? = null) {
val coloredPart = resources.getString(coloredTextRes)
// Insert colored part into the full text
val fullText = resources.getString(fullTextRes, coloredPart)

View File

@ -34,7 +34,6 @@ import im.vector.app.core.utils.deleteAllFiles
import im.vector.app.databinding.ActivityMainBinding
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.ShortcutsHandler
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.PinLocker
@ -43,6 +42,7 @@ import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity2
import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository
import kotlinx.parcelize.Parcelize
@ -222,12 +222,14 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
val intent = when {
args.clearCredentials
&& !ignoreClearCredentials
&& (!args.isUserLoggedOut || args.isAccountDeactivated) ->
&& (!args.isUserLoggedOut || args.isAccountDeactivated) -> {
// User has explicitly asked to log out or deactivated his account
LoginActivity.newIntent(this, null)
navigator.openLogin(this, null)
null
}
args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this)
getSoftLogoutActivityIntent()
args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
SignedOutActivity.newIntent(this)
@ -238,13 +240,23 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
HomeActivity.newIntent(this)
} else {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
getSoftLogoutActivityIntent()
}
else ->
else -> {
// First start, or no active session
LoginActivity.newIntent(this, null)
navigator.openLogin(this, null)
null
}
}
startActivity(intent)
intent?.let { startActivity(it) }
finish()
}
private fun getSoftLogoutActivityIntent(): Intent {
return if (resources.getBoolean(R.bool.useLoginV2)) {
SoftLogoutActivity2.newIntent(this)
} else {
SoftLogoutActivity.newIntent(this)
}
}
}

View File

@ -41,6 +41,7 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import jp.wasabeef.glide.transformations.BlurTransformation
import jp.wasabeef.glide.transformations.ColorFilterTransformation
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.util.MatrixItem
@ -113,6 +114,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.into(imageView)
}
@UiThread
fun render(profileInfo: LoginProfileInfo, imageView: ImageView) {
// Create a Fake MatrixItem, for the placeholder
val matrixItem = MatrixItem.UserItem(
// Need an id starting with @
id = profileInfo.matrixId,
displayName = profileInfo.displayName
)
val placeholder = getPlaceholderDrawable(matrixItem)
GlideApp.with(imageView)
.load(profileInfo.fullAvatarUrl)
.apply(RequestOptions.circleCropTransform())
.placeholder(placeholder)
.into(imageView)
}
@UiThread
fun render(glideRequests: GlideRequests,
matrixItem: MatrixItem,

View File

@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityProgressBinding
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.permalink.PermalinkHandler
import io.reactivex.android.schedulers.AndroidSchedulers
@ -126,9 +125,11 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
* Start the login screen with identity server and home server pre-filled
*/
private fun startLoginActivity(uri: Uri) {
val intent = LoginActivity.newIntent(this, LoginConfig.parse(uri))
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
navigator.openLogin(
context = this,
loginConfig = LoginConfig.parse(uri),
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
)
finish()
}

View File

@ -25,7 +25,6 @@ import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.databinding.FragmentLoginSplashBinding
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
/**

View File

@ -0,0 +1,164 @@
/*
* 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 kotlinx.coroutines.CancellationException
import org.matrix.android.sdk.api.failure.Failure
/**
* Parent Fragment for all the login/registration screens
*/
abstract class AbstractLoginFragment2<VB : ViewBinding> : VectorBaseFragment<VB>(), OnBackPressed {
protected val loginViewModel: LoginViewModel2 by activityViewModel()
private var isResetPasswordStarted = false
// Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
private var displayCancelDialog = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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 CancellationException ->
/* Ignore this error, user has cancelled the action */
Unit
is Failure.UnrecognizedCertificateFailure ->
showUnrecognizedCertificateFailure(throwable)
else ->
onError(throwable)
}
}
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
// Ask the user to accept the certificate
unrecognizedCertificateDialog.show(requireActivity(),
failure.fingerprint,
failure.url,
object : UnrecognizedCertificateDialog.Callback {
override fun onAccept() {
// User accept the certificate
loginViewModel.handle(LoginAction2.UserAcceptCertificate(failure.fingerprint))
}
override fun onIgnore() {
// Cannot happen in this case
}
override fun onReject() {
// Nothing to do in this case
}
})
}
open fun onError(throwable: Throwable) {
super.showFailure(throwable)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {
displayCancelDialog && loginViewModel.isRegistrationStarted -> {
// Ask for confirmation before cancelling the registration
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_signup_cancel_confirmation_title)
.setMessage(R.string.login_signup_cancel_confirmation_content)
.setPositiveButton(R.string.yes) { _, _ ->
displayCancelDialog = false
vectorBaseActivity.onBackPressed()
}
.setNegativeButton(R.string.no, null)
.show()
true
}
displayCancelDialog && isResetPasswordStarted -> {
// Ask for confirmation before cancelling the reset password
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_reset_password_cancel_confirmation_title)
.setMessage(R.string.login_reset_password_cancel_confirmation_content)
.setPositiveButton(R.string.yes) { _, _ ->
displayCancelDialog = false
vectorBaseActivity.onBackPressed()
}
.setNegativeButton(R.string.no, null)
.show()
true
}
else -> {
resetViewModel()
// Do not consume the Back event
false
}
}
}
final override fun invalidate() = withState(loginViewModel) { state ->
// True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
updateWithState(state)
}
open fun updateWithState(state: LoginViewState2) {
// No op by default
}
// Reset any modification on the loginViewModel by the current fragment
abstract fun resetViewModel()
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.content.ComponentName
import android.net.Uri
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.login.hasSso
import im.vector.app.features.login.ssoIdentityProviders
abstract class AbstractSSOLoginFragment2<VB: ViewBinding> : AbstractLoginFragment2<VB>() {
// For sso
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
override fun onStart() {
super.onStart()
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
if (hasSSO) {
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
customTabsClient = client
.also { it.warmup(0L) }
prefetchIfNeeded()
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
.also {
CustomTabsClient.bindCustomTabsService(
requireContext(),
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
}
override fun onStop() {
super.onStop()
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
if (hasSSO) {
customTabsServiceConnection?.let { requireContext().unbindService(it) }
customTabsServiceConnection = null
}
}
private fun prefetchUrl(url: String) {
if (customTabsSession == null) {
customTabsSession = customTabsClient?.newSession(null)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
protected fun openInCustomTab(ssoUrl: String) {
openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl)
}
private fun prefetchIfNeeded() {
withState(loginViewModel) { state ->
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
// in this case we can prefetch (not other cases for privacy concerns)
loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
)
?.let { prefetchUrl(it) }
}
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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 ResetSignin : ResetAction()
object ResetSignup : ResetAction()
object ResetResetPassword : ResetAction()
// Homeserver history
object ClearHomeServerHistory : LoginAction2()
// For the soft logout case
data class SetupSsoForSessionRecovery(val homeServerUrl: String,
val deviceId: String,
val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginAction2()
data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2()
data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2()
// Account customization is over
object Finish : LoginAction2()
}

View File

@ -0,0 +1,410 @@
/*
* 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.extensions.resetBackstack
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.LoginGenericTextInputFormFragmentArgument
import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
import im.vector.app.features.login.TextInputFormFragmentMode
import im.vector.app.features.login.isSupported
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.toLocalizedLoginTerms
import im.vector.app.features.login2.created.AccountCreatedFragment
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, LoginSplashSignUpSignInSelectionFragment2::class.java)
}
private fun handleLoginViewEvents(event: LoginViewEvents2) {
when (event) {
is LoginViewEvents2.RegistrationFlowResult -> {
// Check that all flows are supported by the application
if (event.flowResult.missingStages.any { !it.isSupported() }) {
// Display a popup to propose use web fallback
onRegistrationStageNotSupported()
} else {
if (event.isRegistrationStarted) {
// Go on with registration flow
handleRegistrationNavigation(event.flowResult)
} else {
/*
// First ask for login and password
// I add a tag to indicate that this fragment is a registration stage.
// This way it will be automatically popped in when starting the next registration stage
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,
LoginFragmentSigninUsername2::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.OpenSigninPasswordScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragmentSigninPassword2::class.java,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption)
}
is LoginViewEvents2.OpenSignupPasswordScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragmentSignupPassword2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
}
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragmentSignupUsername2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
}
is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragmentToAny2::class.java,
tag = FRAGMENT_LOGIN_TAG,
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)
is LoginViewEvents2.Finish -> terminate(true)
is LoginViewEvents2.CancelRegistration -> handleCancelRegistration()
}.exhaustive
}
private fun handleCancelRegistration() {
// Cleanup the back stack
resetBackstack()
}
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
if (event.newAccount) {
// Propose to set avatar and display name
// Back on this Fragment will finish the Activity
addFragmentToBackstack(R.id.loginFragmentContainer,
AccountCreatedFragment::class.java,
option = commonOption)
} else {
terminate(false)
}
}
private fun terminate(newAccount: Boolean) {
val intent = HomeActivity.newIntent(
this,
accountCreation = newAccount
)
startActivity(intent)
finish()
}
private fun updateWithState(LoginViewState2: LoginViewState2) {
// Loading
setIsLoading(LoginViewState2.isLoading)
}
// Hack for AccountCreatedFragment
fun setIsLoading(isLoading: Boolean) {
views.loginLoading.isVisible = isLoading
}
private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) {
// Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// And inform the user
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode))
.setPositiveButton(R.string.ok, null)
.show()
}
/**
* Handle the SSO redirection here
*/
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data
?.let { tryOrNull { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
}
private fun onRegistrationStageNotSupported() {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)
.setMessage(getString(R.string.login_registration_not_supported))
.setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment2::class.java,
option = commonOption)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)
.setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
.setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment2::class.java,
option = commonOption)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun handleRegistrationNavigation(flowResult: FlowResult) {
// Complete all mandatory stages first
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
if (mandatoryStage != null) {
doStage(mandatoryStage)
} else {
// Consider optional stages
val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy }
if (optionalStage == null) {
// Should not happen...
} else {
doStage(optionalStage)
}
}
}
private fun doStage(stage: Stage) {
// Ensure there is no fragment for registration stage in the backstack
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
when (stage) {
is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginCaptchaFragment2::class.java,
LoginCaptchaFragmentArgument(stage.publicKey),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginTermsFragment2::class.java,
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
else -> Unit // Should not happen
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
companion object {
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
const val VECTOR_REDIRECT_URL = "element://connect"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity2::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig)
}
}
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import im.vector.app.R
import im.vector.app.core.utils.AssetReader
import im.vector.app.databinding.FragmentLoginCaptchaBinding
import im.vector.app.features.login.JavascriptResponse
import im.vector.app.features.login.LoginCaptchaFragmentArgument
import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber
import java.net.URLDecoder
import java.util.Formatter
import javax.inject.Inject
/**
* In this screen, the user is asked to confirm he is not a robot
*/
class LoginCaptchaFragment2 @Inject constructor(
private val assetReader: AssetReader
) : AbstractLoginFragment2<FragmentLoginCaptchaBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding {
return FragmentLoginCaptchaBinding.inflate(inflater, container, false)
}
private val params: LoginCaptchaFragmentArgument by args()
private var isWebViewLoaded = false
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView(state: LoginViewState2) {
views.loginCaptchaWevView.settings.javaScriptEnabled = true
val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html")
val html = Formatter().format(reCaptchaPage, params.siteKey).toString()
val mime = "text/html"
val encoding = "utf-8"
val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver")
views.loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null)
views.loginCaptchaWevView.requestLayout()
views.loginCaptchaWevView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (!isAdded) {
return
}
// Show loader
views.loginCaptchaProgress.isVisible = true
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
if (!isAdded) {
return
}
// Hide loader
views.loginCaptchaProgress.isVisible = false
}
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
Timber.d("## onReceivedSslError() : ${error.certificate}")
if (!isAdded) {
return
}
AlertDialog.Builder(requireActivity())
.setMessage(R.string.ssl_could_not_verify)
.setPositiveButton(R.string.ssl_trust) { _, _ ->
Timber.d("## onReceivedSslError() : the user trusted")
handler.proceed()
}
.setNegativeButton(R.string.ssl_do_not_trust) { _, _ ->
Timber.d("## onReceivedSslError() : the user did not trust")
handler.cancel()
}
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
handler.cancel()
Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.")
dialog.dismiss()
return@OnKeyListener true
}
false
})
.setCancelable(false)
.show()
}
// common error message
private fun onError(errorMessage: String) {
Timber.e("## onError() : $errorMessage")
// TODO
// Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show()
// on error case, close this activity
// runOnUiThread(Runnable { finish() })
}
@SuppressLint("NewApi")
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
super.onReceivedHttpError(view, request, errorResponse)
if (request.url.toString().endsWith("favicon.ico")) {
// Ignore this error
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
onError(errorResponse.reasonPhrase)
} else {
onError(errorResponse.toString())
}
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
@Suppress("DEPRECATION")
super.onReceivedError(view, errorCode, description, failingUrl)
onError(description)
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url?.startsWith("js:") == true) {
var json = url.substring(3)
var javascriptResponse: JavascriptResponse? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
javascriptResponse = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading(): failed")
}
val response = javascriptResponse?.response
if (javascriptResponse?.action == "verifyCallback" && response != null) {
loginViewModel.handle(LoginAction2.CaptchaDone(response))
}
}
return true
}
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignup)
}
override fun updateWithState(state: LoginViewState2) {
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
}

View File

@ -0,0 +1,181 @@
/*
* 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.airbnb.mvrx.Fail
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.FragmentLoginSigninPassword2Binding
import im.vector.app.features.home.AvatarRenderer
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isInvalidPassword
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
/**
* In this screen:
* - the user is asked for password to sign in to a homeserver.
* - He also can reset his password
*/
class LoginFragmentSigninPassword2 @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : AbstractSSOLoginFragment2<FragmentLoginSigninPassword2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding {
return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupForgottenPasswordButton()
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()
)
avatarRenderer.render(
profileInfo = state.loginProfileInfo() ?: LoginProfileInfo(state.userIdentifier(), null, null),
imageView = views.loginUserIcon
)
views.loginWelcomeBackWarning.isVisible = ((state.loginProfileInfo as? Fail)
?.error as? Failure.ServerError)
?.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
views.passwordField
.textChanges()
.map { it.isNotEmpty() }
.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.ResetSignin)
}
override fun onError(throwable: Throwable) {
if (throwable.isInvalidPassword() && spaceInPassword()) {
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
} else {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
if (state.isLoading) {
// Ensure password is hidden
passwordShown = false
renderPasswordField()
}
}
/**
* Detect if password ends or starts with spaces
*/
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.autofill.HintConstants
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.databinding.FragmentLoginSigninUsername2Binding
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 LoginFragmentSigninUsername2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSigninUsername2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninUsername2Binding {
return FragmentLoginSigninUsername2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupAutoFill()
views.loginChooseAServer.setOnClickListener {
loginViewModel.handle(LoginAction2.ChooseAServerForSignin)
}
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
}
}
private fun submit() {
cleanupUi()
val login = views.loginField.text.toString()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction2.SetUserName(login))
}
}
private fun cleanupUi() {
views.loginSubmit.hideKeyboard()
views.loginFieldTil.error = null
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
views.loginField.textChanges()
.map { it.trim().isNotEmpty() }
.subscribeBy {
views.loginFieldTil.error = null
views.loginSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignin)
}
override fun onError(throwable: Throwable) {
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message.isEmpty()) {
// Login with email, but email unknown
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
} else {
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.FragmentLoginSignupPassword2Binding
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
/**
* In this screen:
* - the user is asked to choose a password to sign up to a homeserver.
*/
class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSignupPassword2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding {
return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupAutoFill()
setupPasswordReveal()
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
}
private fun submit() {
cleanupUi()
val password = views.passwordField.text.toString()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (password.isEmpty()) {
views.passwordFieldTil.error = getString(R.string.error_empty_field_choose_password)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction2.SetUserPassword(password))
}
}
private fun cleanupUi() {
views.loginSubmit.hideKeyboard()
views.passwordFieldTil.error = null
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
views.passwordField.textChanges()
.subscribeBy { password ->
views.passwordFieldTil.error = null
views.loginSubmit.isEnabled = password.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupPasswordReveal() {
passwordShown = false
views.passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
views.passwordReveal.render(passwordShown)
views.passwordField.showPassword(passwordShown)
}
override fun resetViewModel() {
// loginViewModel.handle(LoginAction2.ResetSignup)
}
override fun onError(throwable: Throwable) {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun updateWithState(state: LoginViewState2) {
views.loginMatrixIdentifier.text = state.userIdentifier()
if (state.isLoading) {
// Ensure password is hidden
passwordShown = false
renderPasswordField()
}
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import 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.FragmentLoginSignupUsername2Binding
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 an identifier to sign up to a homeserver.
* - SSO option are displayed if available
*/
class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSignupUsername2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupUsername2Binding {
return FragmentLoginSignupUsername2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupAutoFill()
setupSocialLoginButtons()
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
}
}
private fun setupSocialLoginButtons() {
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP
}
private fun submit() {
cleanupUi()
val login = views.loginField.text.toString().trim()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.loginFieldTil.error = getString(R.string.error_empty_field_choose_user_name)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction2.SetUserName(login))
}
}
private fun cleanupUi() {
views.loginSubmit.hideKeyboard()
views.loginFieldTil.error = null
}
private fun setupUi(state: LoginViewState2) {
views.loginSubtitle.text = getString(R.string.login_signup_to, state.homeServerUrlFromUser.toReducedUrl())
if (state.loginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(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() }
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.ResetSignup)
}
override fun onError(throwable: Throwable) {
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
@SuppressLint("SetTextI18n")
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
}
}

View File

@ -0,0 +1,220 @@
/*
* 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.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.FragmentLoginSigninToAny2Binding
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<FragmentLoginSigninToAny2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninToAny2Binding {
return FragmentLoginSigninToAny2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupForgottenPasswordButton()
setupPasswordReveal()
setupAutoFill()
setupSocialLoginButtons()
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupForgottenPasswordButton() {
views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() }
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
}
}
private fun setupSocialLoginButtons() {
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN
}
private fun submit() {
cleanupUi()
val login = views.loginField.text.toString()
val password = views.passwordField.text.toString()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name)
error++
}
if (password.isEmpty()) {
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction2.LoginWith(login, password))
}
}
private fun cleanupUi() {
views.loginSubmit.hideKeyboard()
views.loginFieldTil.error = null
views.passwordFieldTil.error = null
}
private fun setupUi(state: LoginViewState2) {
views.loginTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
if (state.loginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(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.ResetSignin)
}
override fun onError(throwable: Throwable) {
// Show M_WEAK_PASSWORD error in the password field
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.M_WEAK_PASSWORD) {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
} else {
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message.isEmpty()) {
// Login with email, but email unknown
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
} else {
// Trick to display the error without text.
views.loginFieldTil.error = " "
if (throwable.isInvalidPassword() && spaceInPassword()) {
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
} else {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
}
}
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
if (state.isLoading) {
// Ensure password is hidden
passwordShown = false
renderPasswordField()
}
}
/**
* Detect if password ends or starts with spaces
*/
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
}

View File

@ -0,0 +1,265 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import 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.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginGenericTextInputForm2Binding
import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument
import im.vector.app.features.login.TextInputFormFragmentMode
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
/**
* In this screen, the user is asked for a text input
*/
class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginGenericTextInputForm2Binding>() {
private val params: LoginGenericTextInputFormFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputForm2Binding {
return FragmentLoginGenericTextInputForm2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
setupUi()
setupSubmitButton()
setupTil()
setupAutoFill()
}
private fun setupViews() {
views.loginGenericTextInputFormOtherButton.setOnClickListener { onOtherButtonClicked() }
views.loginGenericTextInputFormSubmit.setOnClickListener { submit() }
views.loginGenericTextInputFormLater.setOnClickListener { submit() }
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginGenericTextInputFormTextInput.setAutofillHints(
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS
TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER
TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP
}
)
}
}
private fun setupTil() {
views.loginGenericTextInputFormTextInput.textChanges()
.subscribe {
views.loginGenericTextInputFormTil.error = null
}
.disposeOnDestroyView()
}
private fun setupUi() {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title_2)
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice_2)
// Text will be updated with the state
views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory
views.loginGenericTextInputFormNotice2.isVisible = false
views.loginGenericTextInputFormTil.hint =
getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint)
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
views.loginGenericTextInputFormOtherButton.isVisible = false
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit)
}
TextInputFormFragmentMode.SetMsisdn -> {
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title_2)
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice_2)
// Text will be updated with the state
views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory
views.loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2))
views.loginGenericTextInputFormTil.hint =
getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint)
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE
views.loginGenericTextInputFormOtherButton.isVisible = false
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit)
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
views.loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title)
views.loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra)
views.loginGenericTextInputFormMandatoryNotice.isVisible = false
views.loginGenericTextInputFormNotice2.isVisible = false
views.loginGenericTextInputFormTil.hint =
getString(R.string.login_msisdn_confirm_hint)
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER
views.loginGenericTextInputFormOtherButton.isVisible = true
views.loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again)
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit)
}
}
}
private fun onOtherButtonClicked() {
when (params.mode) {
TextInputFormFragmentMode.ConfirmMsisdn -> {
loginViewModel.handle(LoginAction2.SendAgainThreePid)
}
else -> {
// Should not happen, button is not displayed
}
}
}
private fun submit() {
cleanupUi()
val text = views.loginGenericTextInputFormTextInput.text.toString()
if (text.isEmpty()) {
// Perform dummy action
loginViewModel.handle(LoginAction2.RegisterDummy)
} else {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Email(text)))
}
TextInputFormFragmentMode.SetMsisdn -> {
getCountryCodeOrShowError(text)?.let { countryCode ->
loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
}
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
loginViewModel.handle(LoginAction2.ValidateThreePid(text))
}
}
}
}
private fun cleanupUi() {
views.loginGenericTextInputFormSubmit.hideKeyboard()
views.loginGenericTextInputFormSubmit.error = null
}
private fun getCountryCodeOrShowError(text: String): String? {
// We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693)
if (text.startsWith("+")) {
try {
val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null)
return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
} catch (e: NumberParseException) {
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other)
}
} else {
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international)
}
// Error
return null
}
private fun setupSubmitButton() {
views.loginGenericTextInputFormSubmit.isEnabled = false
views.loginGenericTextInputFormTextInput.textChanges()
.subscribe { text ->
views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(text)
text?.let { updateSubmitButtons(it) }
}
.disposeOnDestroyView()
}
private fun updateSubmitButtons(text: CharSequence) {
if (params.mandatory) {
views.loginGenericTextInputFormSubmit.isVisible = true
views.loginGenericTextInputFormLater.isVisible = false
} else {
views.loginGenericTextInputFormSubmit.isVisible = text.isNotEmpty()
views.loginGenericTextInputFormLater.isVisible = text.isEmpty()
}
}
private fun isInputValid(input: CharSequence): Boolean {
return if (input.isEmpty() && !params.mandatory) {
true
} else {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> input.isEmail()
TextInputFormFragmentMode.SetMsisdn -> input.isNotBlank()
TextInputFormFragmentMode.ConfirmMsisdn -> input.isNotBlank()
}
}
}
override fun onError(throwable: Throwable) {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
if (throwable.is401()) {
// This is normal use case, we go to the mail waiting screen
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")))
} else {
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
TextInputFormFragmentMode.SetMsisdn -> {
if (throwable.is401()) {
// This is normal use case, we go to the enter code screen
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")))
} else {
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
when {
throwable is Failure.SuccessError ->
// The entered code is not correct
views.loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct)
throwable.is401() ->
// It can happen if user request again the 3pid
Unit
else ->
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignup)
}
override fun updateWithState(state: LoginViewState2) {
views.loginGenericTextInputFormMandatoryNotice.text = when (params.mode) {
TextInputFormFragmentMode.SetEmail -> getString(R.string.login_set_email_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl())
TextInputFormFragmentMode.SetMsisdn -> getString(R.string.login_set_msisdn_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl())
TextInputFormFragmentMode.ConfirmMsisdn -> null
}
}
}

View File

@ -0,0 +1,185 @@
/*
* 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.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.core.utils.autoResetTextInputLayoutErrors
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 passwordShown = 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()
autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil))
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.resetPasswordEmail.setAutofillHints(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS)
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
}
private fun setupUi(state: LoginViewState2) {
views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl())
}
private fun setupSubmitButton() {
views.resetPasswordSubmit.setOnClickListener { submit() }
Observable
.combineLatest(
views.resetPasswordEmail.textChanges().map { it.isEmail() },
views.passwordField.textChanges().map { it.isNotEmpty() },
{ isEmail, isPasswordNotEmpty ->
isEmail && isPasswordNotEmpty
}
)
.subscribeBy {
views.resetPasswordSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
private fun submit() {
cleanupUi()
var error = 0
val email = views.resetPasswordEmail.text.toString()
val password = views.passwordField.text.toString()
if (email.isEmpty()) {
views.resetPasswordEmailTil.error = getString(R.string.auth_reset_password_missing_email)
error++
}
if (password.isEmpty()) {
views.passwordFieldTil.error = getString(R.string.login_please_choose_a_new_password)
error++
}
if (error > 0) {
return
}
if (showWarning) {
// Display a warning as Riot-Web does first
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) { _, _ ->
showWarning = false
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
}
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.ResetResetPassword)
}
override fun onError(throwable: Throwable) {
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
if (state.isLoading) {
// Ensure new password is hidden
passwordShown = false
renderPasswordField()
}
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.FragmentLoginResetPasswordMailConfirmation2Binding
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<FragmentLoginResetPasswordMailConfirmation2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmation2Binding {
return FragmentLoginResetPasswordMailConfirmation2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.resetPasswordMailConfirmationSubmit.setOnClickListener { submit() }
}
private fun setupUi(state: LoginViewState2) {
views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
}
private fun submit() {
loginViewModel.handle(LoginAction2.ResetPasswordMailConfirmed)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetResetPassword)
}
override fun onError(throwable: Throwable) {
// Link in email not yet clicked ?
val message = if (throwable.is401()) {
getString(R.string.auth_reset_password_error_unauthorized)
} else {
errorFormatter.toHumanReadable(throwable)
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.app.databinding.FragmentLoginResetPasswordSuccess2Binding
import javax.inject.Inject
/**
* In this screen, we confirm to the user that his password has been reset
*/
class LoginResetPasswordSuccessFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPasswordSuccess2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordSuccess2Binding {
return FragmentLoginResetPasswordSuccess2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.resetPasswordSuccessSubmit.setOnClickListener { submit() }
}
private fun submit() {
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone))
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetResetPassword)
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.app.R
import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentLoginServerSelection2Binding
import im.vector.app.features.login.EMS_LINK
import javax.inject.Inject
/**
* In this screen, the user will choose between matrix.org, or other type of homeserver
*/
class LoginServerSelectionFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginServerSelection2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerSelection2Binding {
return FragmentLoginServerSelection2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
}
private fun initViews() {
views.loginServerChoiceMatrixOrg.setOnClickListener { selectMatrixOrg() }
views.loginServerChoiceOther.setOnClickListener { selectOther() }
views.loginServerChoiceEmsLearnMore.setTextWithColoredPart(
fullTextRes = R.string.login_server_modular_learn_more_about_ems,
coloredTextRes = R.string.login_server_modular_learn_more,
underline = true
)
views.loginServerChoiceEmsLearnMore.setOnClickListener {
openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
}
}
private fun updateUi(state: LoginViewState2) {
when (state.signMode) {
SignMode2.Unknown -> Unit
SignMode2.SignUp -> {
views.loginServerTitle.setText(R.string.login_please_choose_a_server)
}
SignMode2.SignIn -> {
views.loginServerTitle.setText(R.string.login_please_select_your_server)
}
}
}
private fun selectMatrixOrg() {
views.loginServerChoiceMatrixOrg.isChecked = true
loginViewModel.handle(LoginAction2.ChooseDefaultHomeServer)
}
private fun selectOther() {
views.loginServerChoiceOther.isChecked = true
loginViewModel.handle(LoginAction2.EnterServerUrl)
}
override fun onResume() {
super.onResume()
views.loginServerChoiceMatrixOrg.isChecked = false
views.loginServerChoiceOther.isChecked = false
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
}
override fun updateWithState(state: LoginViewState2) {
updateUi(state)
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import androidx.core.view.isInvisible
import com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import java.net.UnknownHostException
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
/**
* In this screen, the user is prompted to enter a homeserver url
*/
class LoginServerUrlFormFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginServerUrlForm2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerUrlForm2Binding {
return FragmentLoginServerUrlForm2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
setupHomeServerField()
}
private fun setupViews() {
views.loginServerUrlFormClearHistory.setOnClickListener { clearHistory() }
views.loginServerUrlFormSubmit.setOnClickListener { submit() }
}
private fun setupHomeServerField() {
views.loginServerUrlFormHomeServerUrl.textChanges()
.subscribe {
views.loginServerUrlFormHomeServerUrlTil.error = null
views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
}
.disposeOnDestroyView()
views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
views.loginServerUrlFormHomeServerUrl.dismissDropDown()
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupUi(state: LoginViewState2) {
val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList()
views.loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(
requireContext(),
R.layout.item_completion_homeserver,
completions
))
views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
.takeIf { completions.isNotEmpty() }
?: TextInputLayout.END_ICON_NONE
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
}
private fun clearHistory() {
loginViewModel.handle(LoginAction2.ClearHomeServerHistory)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
}
@SuppressLint("SetTextI18n")
private fun submit() {
cleanupUi()
// Static check of homeserver url, empty, malformed, etc.
val serverUrl = views.loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol()
when {
serverUrl.isBlank() -> {
views.loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
}
else -> {
views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/)
loginViewModel.handle(LoginAction2.UpdateHomeServer(serverUrl))
}
}
}
private fun cleanupUi() {
views.loginServerUrlFormSubmit.hideKeyboard()
views.loginServerUrlFormHomeServerUrlTil.error = null
}
override fun onError(throwable: Throwable) {
views.loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection
&& throwable.ioException is UnknownHostException) {
// Invalid homeserver?
getString(R.string.login_error_homeserver_not_found)
} else {
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
getString(R.string.login_registration_disabled)
} else {
errorFormatter.toHumanReadable(throwable)
}
}
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.databinding.FragmentLoginSplash2Binding
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
* This is the new splash screen
*/
class LoginSplashSignUpSignInSelectionFragment2 @Inject constructor(
private val vectorPreferences: VectorPreferences
) : AbstractLoginFragment2<FragmentLoginSplash2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplash2Binding {
return FragmentLoginSplash2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
private fun setupViews() {
views.loginSignupSigninSignUp.setOnClickListener { signUp() }
views.loginSignupSigninSignIn.setOnClickListener { signIn() }
if (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
views.loginSplashVersion.isVisible = true
@SuppressLint("SetTextI18n")
views.loginSplashVersion.text = "Version : ${BuildConfig.VERSION_NAME}\n" +
"Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" +
"Build: ${BuildConfig.BUILD_NUMBER}"
}
}
private fun signUp() {
loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignUp))
}
private fun signIn() {
loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignIn))
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignMode)
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSsoOnly2Binding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSsoOnly2Binding {
return FragmentLoginSsoOnly2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
private fun setupViews() {
views.loginSignupSigninSubmit.setOnClickListener { submit() }
}
private fun setupUi(state: LoginViewState2) {
views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
}
private fun submit() = withState(loginViewModel) { state ->
loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
)
?.let { openInCustomTab(it) }
}
override fun resetViewModel() {
// No op
}
override fun updateWithState(state: LoginViewState2) {
setupUi(state)
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.app.features.login2
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.FlowResult
/**
* Transient events for Login
*/
sealed class LoginViewEvents2 : VectorViewEvents {
data class Failure(val throwable: Throwable) : LoginViewEvents2()
data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents2()
object OutdatedHomeserver : LoginViewEvents2()
// Navigation event
object OpenSigninPasswordScreen : LoginViewEvents2()
object OpenSignupPasswordScreen : LoginViewEvents2()
object OpenSignInEnterIdentifierScreen : LoginViewEvents2()
object OpenSignUpChooseUsernameScreen : LoginViewEvents2()
object OpenSignInWithAnythingScreen : LoginViewEvents2()
object OpenSsoOnlyScreen : LoginViewEvents2()
object OpenServerSelection : LoginViewEvents2()
object OpenHomeServerUrlFormScreen : LoginViewEvents2()
object OpenResetPasswordScreen : LoginViewEvents2()
object OnResetPasswordSendThreePidDone : LoginViewEvents2()
object OnResetPasswordMailConfirmationSuccess : LoginViewEvents2()
object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents2()
object CancelRegistration: LoginViewEvents2()
data class OnLoginModeNotSupported(val supportedTypes: List<String>) : LoginViewEvents2()
data class OnSendEmailSuccess(val email: String) : LoginViewEvents2()
data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents2()
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2()
data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2()
object Finish : LoginViewEvents2()
}

View File

@ -0,0 +1,836 @@
/*
* 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.Loading
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.extensions.tryAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.features.login.HomeServerConnectionConfigFactory
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import java.util.concurrent.CancellationException
/**
*
*/
class LoginViewModel2 @AssistedInject constructor(
@Assisted initialState: LoginViewState2,
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService
) : VectorViewModel<LoginViewState2, LoginAction2, LoginViewEvents2>(initialState) {
@AssistedFactory
interface Factory {
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)
is LoginAction2.Finish -> handleFinish()
}.exhaustive
}
private fun handleFinish() {
// Just post a view Event
_viewEvents.post(LoginViewEvents2.Finish)
}
private fun handleChooseAServerForSignin() {
// Just post a view Event
_viewEvents.post(LoginViewEvents2.OpenServerSelection)
}
private fun handleUserAcceptCertificate(action: LoginAction2.UserAcceptCertificate) {
// It happens when we get the login flow, or during direct authentication.
// So alter the homeserver config and retrieve again the login flow
when (val finalLastAction = lastAction) {
is LoginAction2.UpdateHomeServer -> {
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
}
is LoginAction2.SetUserName ->
handleSetUserNameForSignIn(
finalLastAction,
HomeServerConnectionConfig.Builder()
// Will be replaced by the task
.withHomeServerUri("https://dummy.org")
.withAllowedFingerPrints(listOf(action.fingerprint))
.build()
)
is LoginAction2.SetUserPassword ->
handleSetUserPassword(finalLastAction)
is LoginAction2.LoginWith ->
handleLoginWith(finalLastAction)
}
}
private fun rememberHomeServer(homeServerUrl: String) {
homeServerHistoryService.addHomeServerToHistory(homeServerUrl)
getKnownCustomHomeServersUrls()
}
private fun handleClearHomeServerHistory() {
homeServerHistoryService.clearHistory()
getKnownCustomHomeServersUrls()
}
private fun handleLoginWithToken(action: LoginAction2.LoginWithToken) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
safeLoginWizard.loginWithToken(action.loginToken)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
null
}
?.let { onSessionCreated(it) }
setState { copy(isLoading = false) }
}
}
}
private fun handleSetupSsoForSessionRecovery(action: LoginAction2.SetupSsoForSessionRecovery) {
setState {
copy(
signMode = SignMode2.SignIn,
loginMode = LoginMode.Sso(action.ssoIdentityProviders),
homeServerUrlFromUser = action.homeServerUrl,
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
)
}
}
private fun handleRegisterAction(action: LoginAction2.RegisterAction) {
when (action) {
is LoginAction2.CaptchaDone -> handleCaptchaDone(action)
is LoginAction2.AcceptTerms -> handleAcceptTerms()
is LoginAction2.RegisterDummy -> handleRegisterDummy()
is LoginAction2.AddThreePid -> handleAddThreePid(action)
is LoginAction2.SendAgainThreePid -> handleSendAgainThreePid()
is LoginAction2.ValidateThreePid -> handleValidateThreePid(action)
is LoginAction2.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
is LoginAction2.StopEmailValidationCheck -> handleStopEmailValidationCheck()
}
}
private fun handleCheckIfEmailHasBeenValidated(action: LoginAction2.CheckIfEmailHasBeenValidated) {
// We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
currentJob = executeRegistrationStep(withLoading = false) {
it.checkIfEmailHasBeenValidated(action.delayMillis)
}
}
private fun handleStopEmailValidationCheck() {
currentJob = null
}
private fun handleValidateThreePid(action: LoginAction2.ValidateThreePid) {
currentJob = executeRegistrationStep {
it.handleValidateThreePid(action.code)
}
}
private fun executeRegistrationStep(withLoading: Boolean = true,
block: suspend (RegistrationWizard) -> RegistrationResult): Job {
if (withLoading) {
setState { copy(isLoading = true) }
}
return viewModelScope.launch {
try {
registrationWizard?.let { block(it) }
} catch (failure: Throwable) {
if (failure !is CancellationException) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
}
null
}
?.let { data ->
when (data) {
is RegistrationResult.Success -> onSessionCreated(data.session)
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
}
}
setState { copy(isLoading = false) }
}
}
private fun handleAddThreePid(action: LoginAction2.AddThreePid) {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
registrationWizard?.addThreePid(action.threePid)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
}
setState { copy(isLoading = false) }
}
}
private fun handleSendAgainThreePid() {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
registrationWizard?.sendAgainThreePid()
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
}
setState { copy(isLoading = false) }
}
}
private fun handleAcceptTerms() {
currentJob = executeRegistrationStep {
it.acceptTerms()
}
}
private fun handleRegisterDummy() {
currentJob = executeRegistrationStep {
it.dummy()
}
}
/**
* Check that the user name is available
*/
private fun handleSetUserNameForSignUp(action: LoginAction2.SetUserName) {
setState { copy(isLoading = true) }
val safeRegistrationWizard = registrationWizard ?: error("Invalid")
viewModelScope.launch {
val available = safeRegistrationWizard.registrationAvailable(action.username)
val event = when (available) {
RegistrationAvailability.Available -> {
// Ask for a password
LoginViewEvents2.OpenSignupPasswordScreen
}
is RegistrationAvailability.NotAvailable -> {
LoginViewEvents2.Failure(available.failure)
}
}
_viewEvents.post(event)
setState { copy(isLoading = false) }
}
}
private fun handleCaptchaDone(action: LoginAction2.CaptchaDone) {
currentJob = executeRegistrationStep {
it.performReCaptcha(action.captchaResponse)
}
}
// TODO Update this
private fun handleResetAction(action: LoginAction2.ResetAction) {
// Cancel any request
currentJob = null
when (action) {
LoginAction2.ResetHomeServerUrl -> {
viewModelScope.launch {
authenticationService.reset()
setState {
copy(
homeServerUrlFromUser = null,
homeServerUrl = null,
loginMode = LoginMode.Unknown
)
}
}
}
LoginAction2.ResetSignMode -> {
setState {
copy(
signMode = SignMode2.Unknown,
loginMode = LoginMode.Unknown
)
}
}
LoginAction2.ResetSignin -> {
viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration()
setState {
copy(isLoading = false)
}
}
_viewEvents.post(LoginViewEvents2.CancelRegistration)
}
LoginAction2.ResetSignup -> {
viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration()
setState {
// Always create a new state, to ensure the state is correctly reset
LoginViewState2(
knownCustomHomeServersUrls = knownCustomHomeServersUrls
)
}
}
_viewEvents.post(LoginViewEvents2.CancelRegistration)
}
LoginAction2.ResetResetPassword -> {
setState {
copy(
resetPasswordEmail = null
)
}
}
}
}
private fun handleUpdateSignMode(action: LoginAction2.UpdateSignMode) {
setState {
copy(
signMode = action.signMode
)
}
when (action.signMode) {
SignMode2.SignUp -> _viewEvents.post(LoginViewEvents2.OpenServerSelection)
SignMode2.SignIn -> _viewEvents.post(LoginViewEvents2.OpenSignInEnterIdentifierScreen)
SignMode2.Unknown -> Unit
}
}
private fun handleEnterServerUrl() {
_viewEvents.post(LoginViewEvents2.OpenHomeServerUrlFormScreen)
}
private fun handleInitWith(action: LoginAction2.InitWith) {
loginConfig = action.loginConfig
// If there is a pending email validation continue on this step
try {
if (registrationWizard?.isRegistrationStarted == true) {
currentThreePid?.let {
handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(it)))
}
}
} catch (e: Throwable) {
// NOOP. API is designed to use wizards in a login/registration flow,
// but we need to check the state anyway.
}
}
private fun handleResetPassword(action: LoginAction2.ResetPassword) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPassword(action.email, 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
} ?: return
val loginMode = when {
data.supportedLoginTypes.contains(LoginFlowTypes.SSO)
&& data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
val viewEvent = when (loginMode) {
LoginMode.Password,
is LoginMode.SsoAndPassword -> {
retrieveProfileInfo(action.username)
// We can navigate to the password screen
LoginViewEvents2.OpenSigninPasswordScreen
}
is LoginMode.Sso -> {
LoginViewEvents2.OpenSsoOnlyScreen
}
LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList())
LoginMode.Unknown -> null
}
viewEvent?.let { _viewEvents.post(it) }
val urlFromUser = action.username.substringAfter(":")
setState {
copy(
isLoading = false,
homeServerUrlFromUser = urlFromUser,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode
)
}
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
|| data.isOutdatedHomeserver) {
// Notify the UI
_viewEvents.post(LoginViewEvents2.OutdatedHomeserver)
}
}
private suspend fun retrieveProfileInfo(username: String) {
val safeLoginWizard = loginWizard
if (safeLoginWizard != null) {
setState { copy(loginProfileInfo = Loading()) }
val result = tryAsync {
safeLoginWizard.getProfileInfo(username)
}
setState { copy(loginProfileInfo = result) }
}
}
private fun onDirectLoginError(failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
}
private fun onFlowResponse(flowResult: FlowResult) {
// If dummy stage is mandatory, and password is already sent, do the dummy stage now
if (isRegistrationStarted
&& flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
handleRegisterDummy()
} else {
// Notify the user
_viewEvents.post(LoginViewEvents2.RegistrationFlowResult(flowResult, isRegistrationStarted))
}
}
private suspend fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session)
authenticationService.reset()
session.configureAndStart(applicationContext)
withState { state ->
_viewEvents.post(LoginViewEvents2.OnSessionCreated(state.signMode == SignMode2.SignUp))
}
}
private fun handleWebLoginSuccess(action: LoginAction2.WebLoginSuccess) = withState { state ->
val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl)
if (homeServerConnectionConfigFinal == null) {
// Should not happen
Timber.w("homeServerConnectionConfig is null")
} else {
currentJob = viewModelScope.launch {
try {
authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
null
}
?.let { onSessionCreated(it) }
}
}
}
private fun handleUpdateHomeserver(action: LoginAction2.UpdateHomeServer) {
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
if (homeServerConnectionConfig == null) {
// This is invalid
_viewEvents.post(LoginViewEvents2.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
getLoginFlow(homeServerConnectionConfig)
}
}
private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) = withState { state ->
currentHomeServerConnectionConfig = homeServerConnectionConfig
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration()
val data = try {
authenticationService.getLoginFlow(homeServerConnectionConfig)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
null
} ?: return@launch
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, data.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,
loginMode = loginMode
)
}
}
}
fun getInitialHomeServerUrl(): String? {
return loginConfig?.homeServerUrl
}
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? {
return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId)
}
fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {
return authenticationService.getFallbackUrl(forSignIn, deviceId)
}
}

View File

@ -0,0 +1,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 com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.PersistState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.features.login.LoginMode
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
data class LoginViewState2(
val isLoading: Boolean = false,
// User choices
@PersistState
val signMode: SignMode2 = SignMode2.Unknown,
@PersistState
val userName: String? = null,
@PersistState
val resetPasswordEmail: String? = null,
@PersistState
val homeServerUrlFromUser: String? = null,
// Can be modified after a Wellknown request
@PersistState
val homeServerUrl: String? = null,
// For SSO session recovery
@PersistState
val deviceId: String? = null,
// Network result
val loginProfileInfo: Async<LoginProfileInfo> = Uninitialized,
// Network result
@PersistState
val loginMode: LoginMode = LoginMode.Unknown,
// From database
val knownCustomHomeServersUrls: List<String> = emptyList()
) : MvRxState {
// Pending user identifier
fun userIdentifier(): String {
return if (userName != null && MatrixPatterns.isUserId(userName)) {
userName
} else {
"@$userName:${homeServerUrlFromUser.toReducedUrl()}"
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.args
import im.vector.app.R
import im.vector.app.databinding.FragmentLoginWaitForEmail2Binding
import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
import org.matrix.android.sdk.api.failure.is401
import javax.inject.Inject
/**
* In this screen, the user is asked to check his emails
*/
class LoginWaitForEmailFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginWaitForEmail2Binding>() {
private val params: LoginWaitForEmailFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmail2Binding {
return FragmentLoginWaitForEmail2Binding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
}
override fun onResume() {
super.onResume()
loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(0))
}
override fun onPause() {
super.onPause()
loginViewModel.handle(LoginAction2.StopEmailValidationCheck)
}
private fun setupUi() {
views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice_2, params.email)
}
override fun onError(throwable: Throwable) {
if (throwable.is401()) {
// Try again, with a delay
loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(10_000))
} else {
super.onError(throwable)
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignup)
}
}

View File

@ -0,0 +1,255 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package im.vector.app.features.login2
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.app.R
import im.vector.app.core.utils.AssetReader
import im.vector.app.databinding.FragmentLoginWebBinding
import im.vector.app.features.login.JavascriptResponse
import im.vector.app.features.signout.soft.SoftLogoutAction
import im.vector.app.features.signout.soft.SoftLogoutViewModel
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber
import java.net.URLDecoder
import javax.inject.Inject
/**
* This screen is displayed when the application does not support login flow or registration flow
* of the homeserver, as a fallback to login or to create an account
*/
class LoginWebFragment2 @Inject constructor(
private val assetReader: AssetReader
) : AbstractLoginFragment2<FragmentLoginWebBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWebBinding {
return FragmentLoginWebBinding.inflate(inflater, container, false)
}
private var isWebViewLoaded = false
private var isForSessionRecovery = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.loginWebToolbar)
}
override fun updateWithState(state: LoginViewState2) {
setupTitle(state)
isForSessionRecovery = state.deviceId?.isNotBlank() == true
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
private fun setupTitle(state: LoginViewState2) {
views.loginWebToolbar.title = when (state.signMode) {
SignMode2.SignIn -> getString(R.string.login_signin)
else -> getString(R.string.login_signup)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView(state: LoginViewState2) {
views.loginWebWebView.settings.javaScriptEnabled = true
// Enable local storage to support SSO with Firefox accounts
views.loginWebWebView.settings.domStorageEnabled = true
views.loginWebWebView.settings.databaseEnabled = true
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
views.loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google"
// AppRTC requires third party cookies to work
val cookieManager = android.webkit.CookieManager.getInstance()
// clear the cookies
if (cookieManager == null) {
launchWebView(state)
} else {
if (!cookieManager.hasCookies()) {
launchWebView(state)
} else {
try {
cookieManager.removeAllCookies { launchWebView(state) }
} catch (e: Exception) {
Timber.e(e, " cookieManager.removeAllCookie() fails")
launchWebView(state)
}
}
}
}
private fun launchWebView(state: LoginViewState2) {
val url = loginViewModel.getFallbackUrl(state.signMode == SignMode2.SignIn, state.deviceId) ?: return
views.loginWebWebView.loadUrl(url)
views.loginWebWebView.webViewClient = object : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
error: SslError) {
AlertDialog.Builder(requireActivity())
.setMessage(R.string.ssl_could_not_verify)
.setPositiveButton(R.string.ssl_trust) { _, _ -> handler.proceed() }
.setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> handler.cancel() }
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
handler.cancel()
dialog.dismiss()
return@OnKeyListener true
}
false
})
.setCancelable(false)
.show()
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl)
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnWebLoginError(errorCode, description, failingUrl)))
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
views.loginWebToolbar.subtitle = url
}
override fun onPageFinished(view: WebView, url: String) {
// avoid infinite onPageFinished call
if (url.startsWith("http")) {
// Generic method to make a bridge between JS and the UIWebView
assetReader.readAssetFile("sendObject.js")?.let { view.loadUrl(it) }
if (state.signMode == SignMode2.SignIn) {
// The function the fallback page calls when the login is complete
assetReader.readAssetFile("onLogin.js")?.let { view.loadUrl(it) }
} else {
// MODE_REGISTER
// The function the fallback page calls when the registration is complete
assetReader.readAssetFile("onRegistered.js")?.let { view.loadUrl(it) }
}
}
}
/**
* Example of (formatted) url for MODE_LOGIN:
*
* <pre>
* js:{
* "action":"onLogin",
* "credentials":{
* "user_id":"@user:matrix.org",
* "access_token":"[ACCESS_TOKEN]",
* "home_server":"matrix.org",
* "device_id":"[DEVICE_ID]",
* "well_known":{
* "m.homeserver":{
* "base_url":"https://matrix.org/"
* }
* }
* }
* }
* </pre>
* @param view
* @param url
* @return
*/
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
if (url == null) return super.shouldOverrideUrlLoading(view, url as String?)
if (url.startsWith("js:")) {
var json = url.substring(3)
var javascriptResponse: JavascriptResponse? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java)
javascriptResponse = adapter.fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
}
// succeeds to parse parameters
if (javascriptResponse != null) {
val action = javascriptResponse.action
if (state.signMode == SignMode2.SignIn) {
if (action == "onLogin") {
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
} else {
// MODE_REGISTER
// check the required parameters
if (action == "onRegistered") {
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
}
}
return true
}
return super.shouldOverrideUrlLoading(view, url)
}
}
}
private fun notifyViewModel(credentials: Credentials) {
if (isForSessionRecovery) {
val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
} else {
loginViewModel.handle(LoginAction2.WebLoginSuccess(credentials))
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignin)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {
toolbarButton -> super.onBackPressed(toolbarButton)
views.loginWebWebView.canGoBack() -> views.loginWebWebView.goBack().run { true }
else -> super.onBackPressed(toolbarButton)
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2
enum class SignMode2 {
Unknown,
// Account creation
SignUp,
// Login
SignIn
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2.created
import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction
sealed class AccountCreatedAction : VectorViewModelAction {
data class SetDisplayName(val displayName: String) : AccountCreatedAction()
data class SetAvatar(val avatarUri: Uri, val filename: String) : AccountCreatedAction()
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2.created
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.login2.AbstractLoginFragment2
import im.vector.app.features.login2.LoginAction2
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.login2.LoginViewState2
import org.matrix.android.sdk.api.util.MatrixItem
import java.util.UUID
import javax.inject.Inject
/**
* In this screen:
* - the account has been created and we propose the user to set an avatar and a display name
*/
class AccountCreatedFragment @Inject constructor(
val accountCreatedViewModelFactory: AccountCreatedViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val dateFormatter: VectorDateFormatter,
private val matrixItemColorProvider: MatrixItemColorProvider,
colorProvider: ColorProvider
) : AbstractLoginFragment2<FragmentLoginAccountCreatedBinding>(),
GalleryOrCameraDialogHelper.Listener {
private val viewModel: AccountCreatedViewModel by fragmentViewModel()
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding {
return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupClickListener()
setupSubmitButton()
observeViewEvents()
viewModel.subscribe { invalidateState(it) }
views.loginAccountCreatedTime.text = dateFormatter.format(System.currentTimeMillis(), DateFormatKind.MESSAGE_SIMPLE)
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is AccountCreatedViewEvents.Failure -> displayErrorDialog(it.throwable)
}
}
}
private fun setupClickListener() {
views.loginAccountCreatedMessage.setOnClickListener {
// Update display name
displayDialog()
}
views.loginAccountCreatedAvatar.setOnClickListener {
galleryOrCameraDialogHelper.show()
}
}
private fun displayDialog() = withState(viewModel) { state ->
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val views = DialogBaseEditTextBinding.bind(layout)
views.editText.setText(state.currentUser()?.getBestName().orEmpty())
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_display_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val newName = views.editText.text.toString()
viewModel.handle(AccountCreatedAction.SetDisplayName(newName))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun onImageReady(uri: Uri?) {
uri ?: return
viewModel.handle(AccountCreatedAction.SetAvatar(
avatarUri = uri,
filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
)
}
private fun setupSubmitButton() {
views.loginAccountCreatedLater.setOnClickListener { terminate() }
views.loginAccountCreatedDone.setOnClickListener { terminate() }
}
private fun terminate() {
loginViewModel.handle(LoginAction2.Finish)
}
private fun invalidateState(state: AccountCreatedViewState) {
// Ugly hack...
(activity as? LoginActivity2)?.setIsLoading(state.isLoading)
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)
val user = state.currentUser()
if (user != null) {
avatarRenderer.render(user, views.loginAccountCreatedAvatar)
views.loginAccountCreatedMemberName.text = user.getBestName()
} else {
// Should not happen
views.loginAccountCreatedMemberName.text = state.userId
}
// User color
views.loginAccountCreatedMemberName
.setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(state.userId)))
views.loginAccountCreatedLater.isVisible = state.hasBeenModified.not()
views.loginAccountCreatedDone.isVisible = state.hasBeenModified
}
override fun updateWithState(state: LoginViewState2) {
// No op
}
override fun resetViewModel() {
// No op
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
// Just start the next Activity
terminate()
return false
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.app.features.login2.created
import im.vector.app.core.platform.VectorViewEvents
/**
* Transient events for Account Created
*/
sealed class AccountCreatedViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : AccountCreatedViewEvents()
}

View File

@ -0,0 +1,115 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2.created
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
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.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
import timber.log.Timber
class AccountCreatedViewModel @AssistedInject constructor(
@Assisted initialState: AccountCreatedViewState,
private val session: Session
) : VectorViewModel<AccountCreatedViewState, AccountCreatedAction, AccountCreatedViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel
}
companion object : MvRxViewModelFactory<AccountCreatedViewModel, AccountCreatedViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: AccountCreatedViewState): AccountCreatedViewModel? {
val fragment: AccountCreatedFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.accountCreatedViewModelFactory.create(state)
}
}
init {
setState {
copy(
userId = session.myUserId
)
}
observeUser()
}
private fun observeUser() {
session.rx()
.liveUser(session.myUserId)
.unwrap()
.map {
if (MatrixPatterns.isUserId(it.userId)) {
it.toMatrixItem()
} else {
Timber.w("liveUser() has returned an invalid user: $it")
MatrixItem.UserItem(session.myUserId, null, null)
}
}
.execute {
copy(currentUser = it)
}
}
override fun handle(action: AccountCreatedAction) {
when (action) {
is AccountCreatedAction.SetAvatar -> handleSetAvatar(action)
is AccountCreatedAction.SetDisplayName -> handleSetDisplayName(action)
}
}
private fun handleSetAvatar(action: AccountCreatedAction.SetAvatar) {
setState { copy(isLoading = true) }
viewModelScope.launch {
val result = runCatching { session.updateAvatar(session.myUserId, action.avatarUri, action.filename) }
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
setState {
copy(
isLoading = false,
hasBeenModified = hasBeenModified || result.isSuccess
)
}
}
}
private fun handleSetDisplayName(action: AccountCreatedAction.SetDisplayName) {
setState { copy(isLoading = true) }
viewModelScope.launch {
val result = runCatching { session.setDisplayName(session.myUserId, action.displayName) }
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
setState {
copy(
isLoading = false,
hasBeenModified = hasBeenModified || result.isSuccess
)
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2.created
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.util.MatrixItem
data class AccountCreatedViewState(
val userId: String = "",
val isLoading: Boolean = false,
val currentUser: Async<MatrixItem.UserItem> = Uninitialized,
val hasBeenModified: Boolean = false
) : MvRxState

View File

@ -0,0 +1,119 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login2.terms
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.args
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentLoginTerms2Binding
import im.vector.app.features.login.terms.LocalizedFlowDataLoginTermsChecked
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.LoginTermsViewState
import im.vector.app.features.login.terms.PolicyController
import im.vector.app.features.login2.AbstractLoginFragment2
import im.vector.app.features.login2.LoginAction2
import im.vector.app.features.login2.LoginViewState2
import org.matrix.android.sdk.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<FragmentLoginTerms2Binding>(),
PolicyController.PolicyControllerListener {
private val params: LoginTermsFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTerms2Binding {
return FragmentLoginTerms2Binding.inflate(inflater, container, false)
}
private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
views.loginTermsPolicyList.configureWith(policyController)
policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
params.localizedFlowDataLoginTerms
.forEach {
list.add(LocalizedFlowDataLoginTermsChecked(it))
}
loginTermsViewState = LoginTermsViewState(list)
}
private fun setupViews() {
views.loginTermsSubmit.setOnClickListener { submit() }
}
override fun onDestroyView() {
views.loginTermsPolicyList.cleanup()
policyController.listener = null
super.onDestroyView()
}
private fun renderState() {
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
// Button is enabled only if all checkboxes are checked
views.loginTermsSubmit.isEnabled = loginTermsViewState.allChecked()
}
override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) {
if (isChecked) {
loginTermsViewState.check(localizedFlowDataLoginTerms)
} else {
loginTermsViewState.uncheck(localizedFlowDataLoginTerms)
}
renderState()
}
override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) {
localizedFlowDataLoginTerms.localizedUrl
?.takeIf { it.isNotBlank() }
?.let {
openUrlInChromeCustomTab(requireContext(), null, it)
}
}
private fun submit() {
loginViewModel.handle(LoginAction2.AcceptTerms)
}
override fun updateWithState(state: LoginViewState2) {
policyController.homeServer = state.homeServerUrlFromUser.toReducedUrl()
renderState()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetSignup)
}
}

View File

@ -54,6 +54,9 @@ import im.vector.app.features.home.room.detail.search.SearchActivity
import im.vector.app.features.home.room.detail.search.SearchArgs
import im.vector.app.features.home.room.filtered.FilteredRoomsActivity
import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.AttachmentData
import im.vector.app.features.media.BigImageViewerActivity
@ -99,6 +102,16 @@ class DefaultNavigator @Inject constructor(
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider
) : Navigator {
override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) {
val intent = if (context.resources.getBoolean(R.bool.useLoginV2)) {
LoginActivity2.newIntent(context, loginConfig)
} else {
LoginActivity.newIntent(context, loginConfig)
}
intent.addFlags(flags)
context.startActivity(intent)
}
override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) {
if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) {
fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast())

View File

@ -23,6 +23,7 @@ import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.core.util.Pair
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinMode
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
@ -36,6 +37,8 @@ import org.matrix.android.sdk.api.util.MatrixItem
interface Navigator {
fun openLogin(context: Context, loginConfig: LoginConfig? = null, flags: Int = 0)
fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
sealed class PostSwitchSpaceAction {

View File

@ -26,7 +26,6 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.FragmentProgressBinding
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.LoadingFragment
import im.vector.app.features.login.LoginActivity
import javax.inject.Inject
class PermalinkHandlerActivity : VectorBaseActivity<FragmentProgressBinding>() {
@ -71,9 +70,10 @@ class PermalinkHandlerActivity : VectorBaseActivity<FragmentProgressBinding>() {
}
private fun startLoginActivity() {
val intent = LoginActivity.newIntent(this, null)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
navigator.openLogin(
context = this,
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
)
finish()
}
}

View File

@ -41,7 +41,6 @@ import im.vector.app.databinding.FragmentIncomingShareBinding
import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity
import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
import im.vector.app.features.login.LoginActivity
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -211,9 +210,10 @@ class IncomingShareFragment @Inject constructor(
}
private fun startLoginActivity() {
val intent = LoginActivity.newIntent(requireActivity(), null)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
navigator.openLogin(
context = requireActivity(),
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
)
requireActivity().finish()
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.signout.soft
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.login2.LoginActivity2
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import javax.inject.Inject
/**
* In this screen, the user is viewing a message informing that he has been logged out
* Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free
*
* This is just a copy of SoftLogoutActivity2, which extends LoginActivity2
*/
class SoftLogoutActivity2 : LoginActivity2() {
private val softLogoutViewModel: SoftLogoutViewModel by viewModel()
@Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory
@Inject lateinit var session: Session
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun initUiAndData() {
super.initUiAndData()
softLogoutViewModel.subscribe(this) {
updateWithState(it)
}
softLogoutViewModel.observeViewEvents { handleSoftLogoutViewEvents(it) }
}
private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) {
when (softLogoutViewEvents) {
is SoftLogoutViewEvents.Failure ->
showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable))
is SoftLogoutViewEvents.ErrorNotSameUser -> {
// Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// And inform the user
showError(getString(
R.string.soft_logout_sso_not_same_user_error,
softLogoutViewEvents.currentUserId,
softLogoutViewEvents.newUserId)
)
}
is SoftLogoutViewEvents.ClearData -> {
MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true))
}
}
}
private fun showError(message: String) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun addFirstFragment() {
replaceFragment(R.id.loginFragmentContainer, SoftLogoutFragment::class.java)
}
private fun updateWithState(softLogoutViewState: SoftLogoutViewState) {
if (softLogoutViewState.asyncLoginAction is Success) {
MainActivity.restartApp(this, MainActivityArgs())
}
views.loginLoading.isVisible = softLogoutViewState.isLoading()
}
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, SoftLogoutActivity2::class.java)
}
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
}

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_account_created_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginAccountCreatedSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small"
tools:text="@string/login_account_created_subtitle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_account_created_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_account_created_notice_2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginAccountCreatedMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:background="@drawable/bg_login_server_selector"
android:padding="4dp">
<ImageView
android:id="@+id/loginAccountCreatedAvatar"
android:layout_width="44dp"
android:layout_height="44dp"
android:contentDescription="@string/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/loginAccountCreatedMemberName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/loginAccountCreatedTime"
app:layout_constraintStart_toEndOf="@+id/loginAccountCreatedAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="\@user:domain.org" />
<TextView
android:id="@+id/loginAccountCreatedTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBaseline_toBaselineOf="@+id/loginAccountCreatedMemberName"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@tools:sample/date/hhmm" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_account_created_message"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/loginAccountCreatedMemberName"
app:layout_constraintTop_toBottomOf="@+id/loginAccountCreatedMemberName" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_account_created_instruction"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginAccountCreatedLater"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/later" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginAccountCreatedDone"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/done"
android:visibility="gone"
tools:layout_marginEnd="120dp"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,118 @@
<?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/loginGenericTextInputFormTitle"
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_set_email_title_2" />
<TextView
android:id="@+id/loginGenericTextInputFormNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="@string/login_set_email_notice_2" />
<TextView
android:id="@+id/loginGenericTextInputFormMandatoryNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="26dp"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:visibility="gone"
tools:text="@string/login_set_email_mandatory_notice_2"
tools:visibility="visible" />
<TextView
android:id="@+id/loginGenericTextInputFormNotice2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:visibility="gone"
tools:text="@string/login_set_msisdn_notice2"
tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginGenericTextInputFormTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
tools:hint="@string/login_set_email_optional_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginGenericTextInputFormTextInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:maxLines="1"
tools:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginGenericTextInputFormOtherButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:visibility="gone"
tools:text="@string/login_msisdn_confirm_send_again"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginGenericTextInputFormLater"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/later"
android:visibility="gone"
tools:layout_marginEnd="100dp"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginGenericTextInputFormSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:enabled="false"
tools:ignore="RelativeOverlap"
tools:text="@string/login_set_email_submit" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,122 @@
<?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:layout_marginTop="@dimen/layout_vertical_margin"
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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="16dp"
android:text="@string/login_choose_a_new_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<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_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: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>
<com.google.android.material.button.MaterialButton
android:id="@+id/resetPasswordSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/login_reset_password_submit"
tools:ignore="RelativeOverlap" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,52 @@
<?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:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_reset_password_mail_confirmation_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/resetPasswordMailConfirmationNotice"
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="@string/login_reset_password_mail_confirmation_notice" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_reset_password_mail_confirmation_notice_2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" />
<com.google.android.material.button.MaterialButton
android:id="@+id/resetPasswordMailConfirmationSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="32dp"
android:text="@string/login_reset_password_mail_confirmation_submit" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,52 @@
<?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:layout_marginTop="@dimen/layout_vertical_margin"
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="@dimen/layout_vertical_margin"
android:text="@string/login_reset_password_success_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<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" />
<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="32dp"
android:text="@string/login_reset_password_success_submit" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,142 @@
<?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/loginServerTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_please_choose_a_server"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
tools:ignore="UnknownId" />
<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" />
<im.vector.app.core.platform.CheckableConstraintLayout
android:id="@+id/loginServerChoiceOther"
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_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" />
</im.vector.app.core.platform.CheckableConstraintLayout>
<TextView
android:id="@+id/loginServerChoiceEmsLearnMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/login_server_modular_learn_more_about_ems"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
app:layout_constraintBottom_toBottomOf="@+id/loginServerChoiceEmsText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginServerChoiceEmsText" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,74 @@
<?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="@dimen/layout_vertical_margin"
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:paddingStart="16dp"
android:paddingEnd="0dp"
android:paddingBottom="16dp"
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="4dp"
android:text="@string/login_continue" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,125 @@
<?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="@dimen/layout_vertical_margin"
android:text="@string/login_please_enter_your_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<ImageView
android:id="@+id/loginUserIcon"
android:layout_width="92dp"
android:layout_height="92dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
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:id="@+id/loginWelcomeBackWarning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_unknown_user_warning"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
android:textColor="@color/vector_warning_color_2"
android:visibility="gone"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<im.vector.app.core.ui.views.RevealPasswordImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:tint="?attr/colorAccent" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin">
<com.google.android.material.button.MaterialButton
android:id="@+id/forgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/auth_forgot_password" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/auth_login"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

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

View File

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

View File

@ -0,0 +1,111 @@
<?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="@dimen/layout_vertical_margin"
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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_choose_a_password"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<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>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/_continue"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

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

View File

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

View File

@ -0,0 +1,39 @@
<?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/loginSignupSigninTitle"
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: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_gravity="end"
android:layout_marginTop="38dp"
android:text="@string/login_signin_sso" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -0,0 +1,68 @@
<?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.constraintlayout.widget.ConstraintLayout
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
tools:ignore="MissingConstraints">
<ImageView
style="@style/LoginLogo"
android:layout_marginTop="36dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/loginTermsTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="52dp"
android:layout_marginEnd="36dp"
android:text="@string/login_terms_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginLogo"
tools:ignore="UnknownId" />
<TextView
android:id="@+id/loginTermsNotice"
style="@style/TextAppearance.Vector.Login.Text.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingStart="36dp"
android:paddingEnd="36dp"
android:text="@string/auth_accept_policies"
app:layout_constraintTop_toBottomOf="@+id/loginTermsTitle" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginTermsPolicyList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/loginTermsSubmit"
app:layout_constraintTop_toBottomOf="@+id/loginTermsNotice"
tools:listitem="@layout/item_policy" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginTermsSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_marginEnd="36dp"
android:text="@string/accept"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,55 @@
<?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:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_wait_for_email_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginWaitForEmailNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small"
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="32dp"
android:gravity="start"
android:text="@string/login_wait_for_email_help"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:indeterminate="true" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

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

View File

@ -4,6 +4,7 @@
<!-- Error colors -->
<color name="vector_success_color">#70BF56</color>
<color name="vector_warning_color">#ff4b55</color>
<color name="vector_warning_color_2">#ff812d</color>
<color name="vector_error_color">#ff4b55</color>
<color name="vector_info_color">#2f9edb</color>

View File

@ -0,0 +1,50 @@
<?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_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_server">Please choose a server</string>
<string name="login_please_select_your_server">Please select your server</string>
<string name="login_please_choose_a_password">Please choose a password</string>
<string name="login_please_choose_a_new_password">Please choose a new 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_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_unknown_user_warning">Warning: no profile information can be retrieved with this Matrix identifier. Please check that there is no mistake.</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>
<string name="login_account_created_title">Congratulations!</string>
<string name="login_account_created_subtitle">You account %s has been successfully created.</string>
<string name="login_account_created_notice">To complete your profile, you can set a profile image and/or a display name. This can also be done later from the settings.</string>
<string name="login_account_created_notice_2">This is how your messages will appear:</string>
<string name="login_account_created_message">Hello Matrix world!</string>
<string name="login_account_created_instruction">Click on the image and on your name to configure them.</string>
<string name="login_set_email_title_2">Associate an email</string>
<string name="login_set_email_notice_2">Associate an email to be able to later recover your account, in case you forget your password.</string>
<string name="login_set_email_mandatory_notice_2">The server %s requires you to associate an email to create an account.</string>
<string name="login_set_msisdn_title_2">Associate a phone number</string>
<string name="login_set_msisdn_notice_2">Associate a phone number to optionally allow people you know to discover you.</string>
<string name="login_set_msisdn_mandatory_notice_2">The server %s requires you to associate a phone number to create an account.</string>
<!-- %S will be replaced by the value of login_server_modular_learn_more ("Learn more" in English)-->
<string name="login_server_modular_learn_more_about_ems">%s about Element Matrix Service.</string>
</resources>