Merge pull request #3188 from vector-im/feature/bma/login_v2
Login v2 - WIP
This commit is contained in:
commit
6706a88a21
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
)
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -94,6 +94,12 @@ fun <T : Fragment> AppCompatActivity.addFragmentToBackstack(
|
|||
}
|
||||
}
|
||||
|
||||
fun AppCompatActivity.resetBackstack() {
|
||||
repeat(supportFragmentManager.backStackEntryCount) {
|
||||
supportFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
fun AppCompatActivity.hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.features.login.hasSso
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
|
||||
abstract class AbstractSSOLoginFragment2<VB: ViewBinding> : AbstractLoginFragment2<VB>() {
|
||||
|
||||
// For sso
|
||||
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||
private var customTabsClient: CustomTabsClient? = null
|
||||
private var customTabsSession: CustomTabsSession? = null
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
|
||||
|
||||
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
|
||||
if (packageName != null) {
|
||||
customTabsServiceConnection = object : CustomTabsServiceConnection() {
|
||||
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||
customTabsClient = client
|
||||
.also { it.warmup(0L) }
|
||||
prefetchIfNeeded()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
}
|
||||
.also {
|
||||
CustomTabsClient.bindCustomTabsService(
|
||||
requireContext(),
|
||||
// Despite the API, packageName cannot be null
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
customTabsServiceConnection?.let { requireContext().unbindService(it) }
|
||||
customTabsServiceConnection = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefetchUrl(url: String) {
|
||||
if (customTabsSession == null) {
|
||||
customTabsSession = customTabsClient?.newSession(null)
|
||||
}
|
||||
|
||||
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
|
||||
}
|
||||
|
||||
protected fun openInCustomTab(ssoUrl: String) {
|
||||
openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl)
|
||||
}
|
||||
|
||||
private fun prefetchIfNeeded() {
|
||||
withState(loginViewModel) { state ->
|
||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||
// in this case we can prefetch (not other cases for privacy concerns)
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { prefetchUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordSuccess2Binding
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, we confirm to the user that his password has been reset
|
||||
*/
|
||||
class LoginResetPasswordSuccessFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPasswordSuccess2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordSuccess2Binding {
|
||||
return FragmentLoginResetPasswordSuccess2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
views.resetPasswordSuccessSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetResetPassword)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isInvisible
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* In this screen, the user is prompted to enter a homeserver url
|
||||
*/
|
||||
class LoginServerUrlFormFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginServerUrlForm2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerUrlForm2Binding {
|
||||
return FragmentLoginServerUrlForm2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
setupHomeServerField()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginServerUrlFormClearHistory.setOnClickListener { clearHistory() }
|
||||
views.loginServerUrlFormSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupHomeServerField() {
|
||||
views.loginServerUrlFormHomeServerUrl.textChanges()
|
||||
.subscribe {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
views.loginServerUrlFormHomeServerUrl.dismissDropDown()
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList()
|
||||
views.loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(
|
||||
requireContext(),
|
||||
R.layout.item_completion_homeserver,
|
||||
completions
|
||||
))
|
||||
views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
|
||||
.takeIf { completions.isNotEmpty() }
|
||||
?: TextInputLayout.END_ICON_NONE
|
||||
|
||||
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
|
||||
}
|
||||
|
||||
private fun clearHistory() {
|
||||
loginViewModel.handle(LoginAction2.ClearHomeServerHistory)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
loginViewModel.handle(LoginAction2.ResetHomeServerUrl)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
// Static check of homeserver url, empty, malformed, etc.
|
||||
val serverUrl = views.loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol()
|
||||
|
||||
when {
|
||||
serverUrl.isBlank() -> {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
|
||||
}
|
||||
else -> {
|
||||
views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/)
|
||||
loginViewModel.handle(LoginAction2.UpdateHomeServer(serverUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginServerUrlFormSubmit.hideKeyboard()
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection
|
||||
&& throwable.ioException is UnknownHostException) {
|
||||
// Invalid homeserver?
|
||||
getString(R.string.login_error_homeserver_not_found)
|
||||
} else {
|
||||
if (throwable is Failure.ServerError
|
||||
&& throwable.error.code == MatrixError.M_FORBIDDEN
|
||||
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
|
||||
getString(R.string.login_registration_disabled)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.databinding.FragmentLoginSplash2Binding
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to sign up or to sign in to the homeserver
|
||||
* This is the new splash screen
|
||||
*/
|
||||
class 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to sign up or to sign in to the homeserver
|
||||
*/
|
||||
class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSsoOnly2Binding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSsoOnly2Binding {
|
||||
return FragmentLoginSsoOnly2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginSignupSigninSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupUi(state: LoginViewState2) {
|
||||
views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
|
||||
private fun submit() = withState(loginViewModel) { state ->
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun updateWithState(state: LoginViewState2) {
|
||||
setupUi(state)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue