Social Login

And new custom homeserver completion (and remember history)
This commit is contained in:
Valere 2020-12-03 10:59:08 +01:00 committed by Benoit Marty
parent 163c05d5cf
commit 03428ea9f5
47 changed files with 1001 additions and 220 deletions

View file

@ -25,6 +25,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
@ -49,6 +50,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
private val uiHandler = Handler(Looper.getMainLooper())
@ -71,6 +73,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun rawService() = rawService
fun homeServerHistoryService() = homeServerHistoryService
fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter
}

View file

@ -23,6 +23,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager
@ -47,6 +48,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
init {
Monarchy.init(context)
@ -65,6 +67,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun rawService() = rawService
fun homeServerHistoryService() = homeServerHistoryService
fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter
}

View file

@ -0,0 +1,29 @@
/*
* 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 org.matrix.android.sdk.api.auth
/**
* A simple service to remember homeservers you already connected to.
*/
interface HomeServerHistoryService {
fun getKnownServersUrls(): List<String>
fun addHomeServerToHistory(url: String)
fun clearHistory()
}

View file

@ -0,0 +1,42 @@
/*
* 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 org.matrix.android.sdk.api.auth.data
import android.os.Parcelable
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize
@JsonClass(generateAdapter = true)
@Parcelize
data class IdentityProvider(
/**
* The id field would be opaque with the accepted characters matching unreserved URI characters as defined in RFC3986
* - this was chosen to avoid having to encode special characters in the URL. Max length 128.
*/
@Json(name = "id") val id: String,
/**
* The name field should be the human readable string intended for printing by the client.
* */
@Json(name = "name") val name: String?,
/**
* The icon field is the only optional field and should point to an icon representing the IdP.
* If present then it must be an HTTPS URL to an image resource.
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
*/
@Json(name = "icon") val icon: String?
) : Parcelable

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
sealed class LoginFlowResult {
data class Success(
val supportedLoginTypes: List<String>,
val ssoIdentityProviders: List<IdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean

View file

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@ -278,6 +279,7 @@ internal class DefaultAuthenticationService @Inject constructor(
}
return LoginFlowResult.Success(
loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.identityProvider,
versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl,
!versions.isSupportedBySdk()

View file

@ -0,0 +1,50 @@
/*
* 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 org.matrix.android.sdk.internal.auth
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity
import org.matrix.android.sdk.internal.di.GlobalDatabase
import javax.inject.Inject
class DefaultHomeServerHistoryService @Inject constructor(
@GlobalDatabase private val monarchy: Monarchy
) : HomeServerHistoryService {
override fun getKnownServersUrls(): List<String> {
return monarchy.fetchAllMappedSync(
{ realm ->
realm.where<KnownServerUrlEntity>()
},
{ it.url }
)
}
override fun addHomeServerToHistory(url: String) {
monarchy.writeAsync { realm ->
KnownServerUrlEntity(url).let {
realm.insertOrUpdate(it)
}
}
}
override fun clearHistory() {
monarchy.runTransactionSync { it.where<KnownServerUrlEntity>().findAll().deleteAllFromRealm() }
}
}

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.IdentityProvider
@JsonClass(generateAdapter = true)
internal data class LoginFlowResponse(
@ -34,5 +35,13 @@ internal data class LoginFlow(
* The login type. This is supplied as the type when logging in.
*/
@Json(name = "type")
val type: String?
val type: String?,
/**
* Augments m.login.sso flow discovery definition to include metadata on the supported IDPs
* the client can show a button for each of the supported providers
* See MSC #2858
*/
@Json(name = "identity_providers")
val identityProvider: List<IdentityProvider>?
)

View file

@ -51,6 +51,18 @@ data class RegistrationFlowResponse(
* The information that the client will need to know in order to use a given type of authentication.
* For each login stage type presented, that type may be present as a key in this dictionary.
* For example, the public key of reCAPTCHA stage could be given here.
* other example
* "params": {
* "m.login.sso": {
* "identity_providers": [
* {
* "id": "google",
* "name": "Google",
* "icon": "https://..."
* }
* ]
* }
* }
*/
@Json(name = "params")
val params: JsonDict? = null

View file

@ -0,0 +1,27 @@
/*
* Copyright 2020 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.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class KnownServerUrlEntity(
@PrimaryKey
var url: String = ""
) : RealmObject() {
companion object
}

View file

@ -25,6 +25,7 @@ import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule
@ -62,6 +63,8 @@ internal interface MatrixComponent {
fun rawService(): RawService
fun homeServerHistoryService(): HomeServerHistoryService
fun context(): Context
fun matrixConfiguration(): MatrixConfiguration

View file

@ -0,0 +1,39 @@
/*
* 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 org.matrix.android.sdk.internal.raw
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields
import timber.log.Timber
internal object GlobalRealmMigration : RealmMigration {
// Current schema version
const val SCHEMA_VERSION = 1L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
realm.schema.create("KnownServerUrlEntity")
.addField(KnownServerUrlEntityFields.URL, String::class.java)
}
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.raw
import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
/**
@ -24,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RawCacheEntity
*/
@RealmModule(library = true,
classes = [
RawCacheEntity::class
RawCacheEntity::class,
KnownServerUrlEntity::class
])
internal class GlobalRealmModule

View file

@ -24,7 +24,9 @@ import dagger.Module
import dagger.Provides
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.auth.DefaultHomeServerHistoryService
import org.matrix.android.sdk.internal.database.RealmKeysUtils
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.di.MatrixScope
@ -57,6 +59,8 @@ internal abstract class RawModule {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-global.realm")
.schemaVersion(GlobalRealmMigration.SCHEMA_VERSION)
.migration(GlobalRealmMigration)
.modules(GlobalRealmModule())
.build()
}
@ -77,4 +81,7 @@ internal abstract class RawModule {
@Binds
abstract fun bindCleanRawCacheTask(task: DefaultCleanRawCacheTask): CleanRawCacheTask
@Binds
abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService
}

View file

@ -64,7 +64,6 @@ import im.vector.app.features.login.LoginResetPasswordSuccessFragment
import im.vector.app.features.login.LoginServerSelectionFragment
import im.vector.app.features.login.LoginServerUrlFormFragment
import im.vector.app.features.login.LoginSignUpSignInSelectionFragment
import im.vector.app.features.login.LoginSignUpSignInSsoFragment
import im.vector.app.features.login.LoginSplashFragment
import im.vector.app.features.login.LoginWaitForEmailFragment
import im.vector.app.features.login.LoginWebFragment
@ -229,11 +228,11 @@ interface FragmentModule {
@IntoMap
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSignUpSignInSsoFragment::class)
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
//
// @Binds
// @IntoMap
// @FragmentKey(LoginSignUpSignInSsoFragment::class)
// fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
@Binds
@IntoMap

View file

@ -59,6 +59,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.ui.UiStateRepository
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import javax.inject.Singleton
@ -127,6 +128,8 @@ interface VectorComponent {
fun rawService(): RawService
fun homeServerHistoryService(): HomeServerHistoryService
fun bugReporter(): BugReporter
fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler

View file

@ -33,6 +33,7 @@ import im.vector.app.features.ui.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
@ -85,6 +86,12 @@ abstract class VectorModule {
fun providesRawService(matrix: Matrix): RawService {
return matrix.rawService()
}
@Provides
@JvmStatic
fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService {
return matrix.homeServerHistoryService()
}
}
@Binds

View file

@ -18,6 +18,7 @@ package im.vector.app.features.login
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.IdentityProvider
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
@ -60,7 +61,7 @@ sealed class LoginAction : VectorViewModelAction {
object ResetResetPassword : ResetAction()
// For the soft logout case
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction()
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String, val identityProvider: List<IdentityProvider>?) : LoginAction()
data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()

View file

@ -112,7 +112,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.RegistrationFlowResult -> {
is LoginViewEvents.RegistrationFlowResult -> {
// Check that all flows are supported by the application
if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) {
// Display a popup to propose use web fallback
@ -133,7 +133,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
}
}
}
is LoginViewEvents.OutdatedHomeserver -> {
is LoginViewEvents.OutdatedHomeserver -> {
AlertDialog.Builder(this)
.setTitle(R.string.login_error_outdated_homeserver_title)
.setMessage(R.string.login_error_outdated_homeserver_warning_content)
@ -141,7 +141,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
.show()
Unit
}
is LoginViewEvents.OpenServerSelection ->
is LoginViewEvents.OpenServerSelection ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java,
option = { ft ->
@ -153,28 +153,24 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents)
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents)
is LoginViewEvents.OnLoginFlowRetrieved ->
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents)
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents)
is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer,
if (loginViewEvents.isSso) {
LoginSignUpSignInSsoFragment::class.java
} else {
LoginSignUpSignInSelectionFragment::class.java
},
LoginSignUpSignInSelectionFragment::class.java,
option = commonOption)
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked ->
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordFragment::class.java,
option = commonOption)
is LoginViewEvents.OnResetPasswordSendThreePidDone -> {
is LoginViewEvents.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordMailConfirmationFragment::class.java,
option = commonOption)
}
is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> {
is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordSuccessFragment::class.java,
@ -184,20 +180,20 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
// Go back to the login fragment
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
}
is LoginViewEvents.OnSendEmailSuccess ->
is LoginViewEvents.OnSendEmailSuccess ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWaitForEmailFragment::class.java,
LoginWaitForEmailFragmentArgument(loginViewEvents.email),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is LoginViewEvents.OnSendMsisdnSuccess ->
is LoginViewEvents.OnSendMsisdnSuccess ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is LoginViewEvents.Failure,
is LoginViewEvents.Loading ->
is LoginViewEvents.Loading ->
// This is handled by the Fragments
Unit
}.exhaustive
@ -234,25 +230,26 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
when (loginViewEvents.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.EMS,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment::class.java,
option = commonOption)
ServerType.Unknown -> Unit /* Should not happen */
ServerType.Unknown -> Unit /* Should not happen */
}
}
private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state ->
// state.signMode could not be ready yet. So use value from the ViewEvent
when (loginViewEvents.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> {
// This is managed by the LoginViewEvents
}
SignMode.SignIn -> {
SignMode.SignIn -> {
// It depends on the LoginMode
when (state.loginMode) {
LoginMode.Unknown,
LoginMode.Sso -> error("Developer error")
is LoginMode.Sso -> error("Developer error")
is LoginMode.SsoAndPassword,
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG,
@ -331,17 +328,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
LoginCaptchaFragmentArgument(stage.publicKey),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginTermsFragment::class.java,
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
@ -350,6 +347,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
}
}
override fun onBackPressed() {
super.onBackPressed()
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

View file

@ -37,6 +37,7 @@ import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_login.*
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -79,15 +80,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private fun setupAutoFill(state: LoginViewState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP
}
SignMode.SignIn,
SignMode.SignInWithMatrixId -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN
}
}.exhaustive
}
@ -142,9 +145,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginPasswordNotice.isVisible = true
} else {
val resId = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_to
SignMode.SignIn -> R.string.login_connect_to
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_to
SignMode.SignIn -> R.string.login_connect_to
SignMode.SignInWithMatrixId -> R.string.login_connect_to
}
@ -155,20 +158,28 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.EMS -> {
ServerType.EMS -> {
loginServerIcon.isVisible = true
loginServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
loginTitle.text = getString(resId, "Element Matrix Services")
loginNotice.text = getString(R.string.login_server_modular_text)
}
ServerType.Other -> {
ServerType.Other -> {
loginServerIcon.isVisible = false
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_other_text)
}
ServerType.Unknown -> Unit /* Should not happen */
ServerType.Unknown -> Unit /* Should not happen */
}
loginPasswordNotice.isVisible = false
if (state.loginMode is LoginMode.SsoAndPassword) {
loginSocialLoginContainer.isVisible = true
loginSocialLoginButtons.identityProviders = state.loginMode.identityProviders
} else {
loginSocialLoginContainer.isVisible = false
loginSocialLoginButtons.identityProviders = null
}
}
}
@ -257,7 +268,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
passwordShown = false
renderPasswordField()
}
is Fail -> {
is Fail -> {
val error = state.asyncLoginAction.error
if (error is Failure.ServerError
&& error.error.code == MatrixError.M_FORBIDDEN

View file

@ -16,9 +16,31 @@
package im.vector.app.features.login
enum class LoginMode {
Unknown,
Password,
Sso,
Unsupported
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.auth.data.IdentityProvider
sealed class LoginMode : Parcelable
/** because persist state */ {
@Parcelize object Unknown : LoginMode()
@Parcelize object Password : LoginMode()
@Parcelize data class Sso(val identityProviders: List<IdentityProvider>?) : LoginMode()
@Parcelize data class SsoAndPassword(val identityProviders: List<IdentityProvider>?) : LoginMode()
@Parcelize object Unsupported : LoginMode()
}
fun LoginMode.ssoProviders() : List<IdentityProvider>? {
return when (this) {
is LoginMode.Sso -> identityProviders
is LoginMode.SsoAndPassword -> identityProviders
else -> null
}
}
fun LoginMode.hasSso() : Boolean {
return when (this) {
is LoginMode.Sso -> true
is LoginMode.SsoAndPassword -> true
else -> false
}
}

View file

@ -83,7 +83,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
if (state.loginMode != LoginMode.Unknown) {
// LoginFlow for matrix.org has been retrieved
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode is LoginMode.Sso)))
}
}
}

View file

@ -20,8 +20,10 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import androidx.core.view.isVisible
import butterknife.OnClick
import com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
@ -55,6 +57,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
loginServerUrlFormHomeServerUrl.dismissDropDown()
submit()
return@setOnEditorActionListener true
}
@ -81,6 +84,13 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice)
}
}
val completions = state.knownCustomHomeServersUrls
loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(requireContext(), android.R.layout.select_dialog_item,
completions
))
loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
.takeIf { completions.isNotEmpty() }
?: TextInputLayout.END_ICON_NONE
}
@OnClick(R.id.loginServerUrlFormLearnMore)
@ -105,7 +115,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
}
else -> {
loginServerUrlFormHomeServerUrl.setText(serverUrl)
loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/)
loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl))
}
}
@ -131,7 +141,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
if (state.loginMode != LoginMode.Unknown) {
// The home server url is valid
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode is LoginMode.Sso)))
}
}
}

View file

@ -16,21 +16,36 @@
package im.vector.app.features.login
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.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import org.matrix.android.sdk.api.auth.data.IdentityProvider
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
// Map of sso urls by providers if any
private var ssoUrls = emptyMap<String?, String>().toMutableMap()
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
protected fun setupUi(state: LoginViewState) {
private fun setupUi(state: LoginViewState) {
when (state.serverType) {
ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
@ -51,16 +66,110 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
}
ServerType.Unknown -> Unit /* Should not happen */
}
val identityProviders = state.loginMode.ssoProviders()
if (state.loginMode.hasSso() && identityProviders.isNullOrEmpty().not()) {
loginSignupSigninSignInSocialLoginContainer.isVisible = true
loginSignupSigninSocialLoginButtons.identityProviders = identityProviders
loginSignupSigninSocialLoginButtons.listener = object: SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: IdentityProvider) {
ssoUrls[id.id]?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
}
}
} else {
loginSignupSigninSignInSocialLoginContainer.isVisible = false
loginSignupSigninSocialLoginButtons.identityProviders = null
}
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
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) }
// prefetch urls
prefetchSsoUrls()
}
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 prefetchSsoUrls() = withState(loginViewModel) { state ->
val providers = state.loginMode.ssoProviders()
if (providers.isNullOrEmpty()) {
state.getSsoUrl(null).let {
ssoUrls[null] = it
prefetchUrl(it)
}
} else {
providers.forEach { identityProvider ->
state.getSsoUrl(identityProvider.id).let {
ssoUrls[identityProvider.id] = it
// we don't prefetch for privacy reasons
}
}
}
}
private fun prefetchUrl(url: String) {
if (customTabsSession == null) {
customTabsSession = customTabsClient?.newSession(null)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
private fun setupButtons(state: LoginViewState) {
when (state.loginMode) {
is LoginMode.Sso -> {
// change to only one button that is sign in with sso
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
}
else -> {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
}
}
@OnClick(R.id.loginSignupSigninSubmit)
open fun submit() {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
fun submit() = withState(loginViewModel) { state ->
if (state.loginMode is LoginMode.Sso) {
ssoUrls[null]?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
} else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
Unit
}
@OnClick(R.id.loginSignupSigninSignIn)
@ -74,6 +183,6 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons()
setupButtons(state)
}
}

View file

@ -1,99 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login
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.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in using SSO
* This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened.
*/
open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() {
private var ssoUrl: String? = null
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
override fun onStart() {
super.onStart()
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) }
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
.also {
CustomTabsClient.bindCustomTabsService(
requireContext(),
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
private fun prefetchUrl(url: String) {
if (ssoUrl != null) return
ssoUrl = url
if (customTabsSession == null) {
customTabsSession = customTabsClient?.newSession(null)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
override fun onStop() {
super.onStop()
customTabsServiceConnection?.let { requireContext().unbindService(it) }
customTabsServiceConnection = null
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
}
override fun submit() {
ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons()
prefetchUrl(state.getSsoUrl())
}
}

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.configureAndStart
@ -38,6 +39,7 @@ import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.features.signout.soft.SoftLogoutActivity
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
@ -63,7 +65,8 @@ class LoginViewModel @AssistedInject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider
private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService
) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedInject.Factory
@ -71,12 +74,21 @@ class LoginViewModel @AssistedInject constructor(
fun create(initialState: LoginViewState): LoginViewModel
}
init {
if (BuildConfig.DEBUG) {
homeServerHistoryService.addHomeServerToHistory("http://10.0.2.2:8080")
}
setState {
copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls())
}
}
companion object : MvRxViewModelFactory<LoginViewModel, LoginViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? {
return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) {
is LoginActivity -> activity.loginViewModelFactory.create(state)
is LoginActivity -> activity.loginViewModelFactory.create(state)
is SoftLogoutActivity -> activity.loginViewModelFactory.create(state)
else -> error("Invalid Activity")
}
@ -108,20 +120,20 @@ class LoginViewModel @AssistedInject constructor(
override fun handle(action: LoginAction) {
when (action) {
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action)
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action)
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
}.exhaustive
}
@ -129,11 +141,13 @@ class LoginViewModel @AssistedInject constructor(
// It happen 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 LoginAction.UpdateHomeServer ->
is LoginAction.UpdateHomeServer -> {
rememberHomeServer(finalLastAction.homeServerUrl)
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
is LoginAction.LoginOrRegister ->
}
is LoginAction.LoginOrRegister ->
handleDirectLogin(
finalLastAction,
HomeServerConnectionConfig.Builder()
@ -145,6 +159,13 @@ class LoginViewModel @AssistedInject constructor(
}
}
private fun rememberHomeServer(homeServerUrl: String) {
homeServerHistoryService.addHomeServerToHistory(homeServerUrl)
setState {
copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls())
}
}
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
val safeLoginWizard = loginWizard
@ -184,7 +205,7 @@ class LoginViewModel @AssistedInject constructor(
setState {
copy(
signMode = SignMode.SignIn,
loginMode = LoginMode.Sso,
loginMode = LoginMode.Sso(action.identityProvider),
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
)
@ -193,14 +214,14 @@ class LoginViewModel @AssistedInject constructor(
private fun handleRegisterAction(action: LoginAction.RegisterAction) {
when (action) {
is LoginAction.CaptchaDone -> handleCaptchaDone(action)
is LoginAction.AcceptTerms -> handleAcceptTerms()
is LoginAction.RegisterDummy -> handleRegisterDummy()
is LoginAction.AddThreePid -> handleAddThreePid(action)
is LoginAction.SendAgainThreePid -> handleSendAgainThreePid()
is LoginAction.ValidateThreePid -> handleValidateThreePid(action)
is LoginAction.CaptchaDone -> handleCaptchaDone(action)
is LoginAction.AcceptTerms -> handleAcceptTerms()
is LoginAction.RegisterDummy -> handleRegisterDummy()
is LoginAction.AddThreePid -> handleAddThreePid(action)
is LoginAction.SendAgainThreePid -> handleSendAgainThreePid()
is LoginAction.ValidateThreePid -> handleValidateThreePid(action)
is LoginAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
is LoginAction.StopEmailValidationCheck -> handleStopEmailValidationCheck()
is LoginAction.StopEmailValidationCheck -> handleStopEmailValidationCheck()
}
}
@ -237,7 +258,7 @@ class LoginViewModel @AssistedInject constructor(
}
when (data) {
is RegistrationResult.Success -> onSessionCreated(data.session)
is RegistrationResult.Success -> onSessionCreated(data.session)
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
}
}
@ -337,7 +358,7 @@ class LoginViewModel @AssistedInject constructor(
)
}
}
LoginAction.ResetHomeServerUrl -> {
LoginAction.ResetHomeServerUrl -> {
authenticationService.reset()
setState {
@ -350,7 +371,7 @@ class LoginViewModel @AssistedInject constructor(
)
}
}
LoginAction.ResetSignMode -> {
LoginAction.ResetSignMode -> {
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
@ -360,7 +381,7 @@ class LoginViewModel @AssistedInject constructor(
)
}
}
LoginAction.ResetLogin -> {
LoginAction.ResetLogin -> {
authenticationService.cancelPendingLoginOrRegistration()
setState {
@ -370,7 +391,7 @@ class LoginViewModel @AssistedInject constructor(
)
}
}
LoginAction.ResetResetPassword -> {
LoginAction.ResetResetPassword -> {
setState {
copy(
asyncResetPassword = Uninitialized,
@ -390,10 +411,10 @@ class LoginViewModel @AssistedInject constructor(
}
when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow()
SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignUp -> startRegistrationFlow()
SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit
SignMode.Unknown -> Unit
}
}
@ -405,12 +426,12 @@ class LoginViewModel @AssistedInject constructor(
}
when (action.serverType) {
ServerType.Unknown -> Unit /* Should not happen */
ServerType.Unknown -> Unit /* Should not happen */
ServerType.MatrixOrg ->
// Request login flow here
handle(LoginAction.UpdateHomeServer(matrixOrgUrl))
ServerType.EMS,
ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
}.exhaustive
}
@ -514,9 +535,9 @@ class LoginViewModel @AssistedInject constructor(
private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state ->
when (state.signMode) {
SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action)
SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action)
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
}.exhaustive
}
@ -713,11 +734,11 @@ class LoginViewModel @AssistedInject constructor(
private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) {
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
if (homeServerConnectionConfig == null) {
// This is invalid
_viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
rememberHomeServer(action.homeServerUrl)
getLoginFlow(homeServerConnectionConfig)
}
}
@ -755,9 +776,11 @@ class LoginViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
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
}
// FIXME We should post a view event here normally?

View file

@ -51,7 +51,8 @@ data class LoginViewState(
val loginMode: LoginMode = LoginMode.Unknown,
@PersistState
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
val loginModeSupportedTypes: List<String> = emptyList()
val loginModeSupportedTypes: List<String> = emptyList(),
val knownCustomHomeServersUrls: List<String> = emptyList()
) : MvRxState {
fun isLoading(): Boolean {
@ -68,10 +69,13 @@ data class LoginViewState(
return asyncLoginAction is Success
}
fun getSsoUrl(): String {
fun getSsoUrl(providerId: String?): String {
return buildString {
append(homeServerUrl?.trim { it == '/' })
append(SSO_REDIRECT_PATH)
if (providerId != null) {
append("/$providerId")
}
// Set a redirect url we will intercept later
appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL)
deviceId?.takeIf { it.isNotBlank() }?.let {

View file

@ -0,0 +1,149 @@
/*
* 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.login
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.LinearLayout
import androidx.core.view.children
import com.google.android.material.button.MaterialButton
import im.vector.app.R
import org.matrix.android.sdk.api.auth.data.IdentityProvider
class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
interface InteractionListener {
fun onProviderSelected(id: IdentityProvider)
}
enum class Mode {
MODE_SIGN_IN,
MODE_SIGN_UP,
MODE_CONTINUE,
}
var identityProviders: List<IdentityProvider>? = null
set(newProviders) {
if (newProviders != identityProviders) {
field = newProviders
update()
}
}
var mode: Mode = Mode.MODE_CONTINUE
set(value) {
if (value != mode) {
field = value
update()
}
}
var listener: InteractionListener? = null
private fun update() {
val cachedViews = emptyMap<String, MaterialButton>().toMutableMap()
children.filterIsInstance<MaterialButton>().forEach {
cachedViews[it.getTag(R.id.loginSignupSigninSocialLoginButtons)?.toString() ?: ""] = it
}
removeAllViews()
if (identityProviders.isNullOrEmpty()) {
return
}
identityProviders?.forEach { identityProvider ->
// Use some heuristic to render buttons according to branding guidelines
val cached = cachedViews[identityProvider.id]
val button: MaterialButton = if (cached != null) {
cached
} else {
when (identityProvider.id) {
"google" -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_google_style)
}
"github" -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_github_style)
}
"apple" -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style)
}
"facebook" -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style)
}
"twitter" -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style)
}
else -> {
MaterialButton(context, null, R.attr.materialButtonStyle).apply {
transformationMethod = null
textAlignment = View.TEXT_ALIGNMENT_CENTER
}
}
}
}
button.text = getButtonTitle(identityProvider)
button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id)
button.setOnClickListener {
listener?.onProviderSelected(identityProvider)
}
addView(button)
}
}
private fun getButtonTitle(provider: IdentityProvider): String {
return when (mode) {
Mode.MODE_SIGN_IN -> context.getString(R.string.login_social_signin_with, provider.name)
Mode.MODE_SIGN_UP -> context.getString(R.string.login_social_signup_with, provider.name)
Mode.MODE_CONTINUE -> context.getString(R.string.login_social_continue_with, provider.name)
}
}
init {
this.orientation = VERTICAL
gravity = Gravity.CENTER
clipToPadding = false
clipChildren = false
@SuppressLint("SetTextI18n")
if (isInEditMode) {
identityProviders = listOf(
IdentityProvider("google", "Google", null),
IdentityProvider("facebook", "Facebook", null),
IdentityProvider("apple", "Apple", null),
IdentityProvider("github", "Github", null),
IdentityProvider("twitter", "Twitter", null),
IdentityProvider("Custom_pro", "SSO", null)
)
}
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0)
val modeAttr = typedArray.getInt(R.styleable.SocialLoginButtonsView_signMode, 2)
mode = when (modeAttr) {
0 -> Mode.MODE_SIGN_IN
1 -> Mode.MODE_SIGN_UP
else -> Mode.MODE_CONTINUE
}
typedArray.recycle()
}
fun dpToPx(dp: Int): Int {
val resources = context.resources
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
}
}

View file

@ -107,7 +107,7 @@ class SoftLogoutController @Inject constructor(
}
is Success -> {
when (state.asyncHomeServerLoginFlowRequest.invoke()) {
LoginMode.Password -> {
LoginMode.Password -> {
loginPasswordFormItem {
id("passwordForm")
stringProvider(stringProvider)
@ -120,21 +120,23 @@ class SoftLogoutController @Inject constructor(
submitClickListener { password -> listener?.signinSubmit(password) }
}
}
LoginMode.Sso -> {
is LoginMode.Sso -> {
loginCenterButtonItem {
id("sso")
text(stringProvider.getString(R.string.login_signin_sso))
listener { listener?.signinFallbackSubmit() }
}
}
LoginMode.Unsupported -> {
is LoginMode.SsoAndPassword -> {
}
LoginMode.Unsupported -> {
loginCenterButtonItem {
id("fallback")
text(stringProvider.getString(R.string.login_signin))
listener { listener?.signinFallbackSubmit() }
}
}
LoginMode.Unknown -> Unit // Should not happen
LoginMode.Unknown -> Unit // Should not happen
}
}
}

View file

@ -54,14 +54,27 @@ class SoftLogoutFragment @Inject constructor(
softLogoutViewModel.subscribe(this) { softLogoutViewState ->
softLogoutController.update(softLogoutViewState)
when (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) {
LoginMode.Sso,
when (val mode = softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) {
is LoginMode.SsoAndPassword -> {
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId,
mode.identityProviders
))
}
is LoginMode.Sso -> {
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId,
mode.identityProviders
))
}
LoginMode.Unsupported -> {
// Prepare the loginViewModel for a SSO/login fallback recovery
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId
softLogoutViewState.deviceId,
null
))
}
else -> Unit

View file

@ -105,9 +105,11 @@ class SoftLogoutViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
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
}
setState {

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#4285F4" android:state_enabled="true"/>
<item android:color="@color/riotx_disabled_view_color_light" android:state_enabled="false"/>
<item android:color="#3367D6" android:state_pressed="true"/>
<item android:color="#4285F4" android:state_focused="true"/>
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_enabled="true"/>
<item android:color="@color/riotx_disabled_view_color_light" android:state_enabled="false"/>
<item android:color="@color/riotx_disabled_view_color_light" android:state_pressed="true"/>
</selector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M19.934,15.416C20.174,15.416 20.6,15.314 21.212,15.11C21.824,14.906 22.346,14.804 22.778,14.804C23.498,14.804 24.14,14.996 24.704,15.38C24.86,15.5 25.016,15.632 25.172,15.776C25.328,15.92 25.484,16.088 25.64,16.28C25.172,16.688 24.83,17.048 24.614,17.36C24.218,17.912 24.02,18.53 24.02,19.214C24.02,19.958 24.23,20.63 24.65,21.23C25.058,21.818 25.532,22.196 26.072,22.364C25.952,22.724 25.802,23.093 25.622,23.471C25.442,23.849 25.226,24.23 24.974,24.614C24.206,25.778 23.432,26.36 22.652,26.36C22.328,26.36 21.902,26.264 21.374,26.072C20.858,25.892 20.408,25.802 20.024,25.802C19.628,25.802 19.196,25.898 18.728,26.09C18.224,26.294 17.828,26.396 17.54,26.396C16.628,26.396 15.728,25.622 14.84,24.074C13.952,22.538 13.508,21.026 13.508,19.538C13.508,18.17 13.844,17.048 14.516,16.172C15.2,15.308 16.058,14.876 17.09,14.876C17.534,14.876 18.068,14.966 18.692,15.146C19.328,15.326 19.742,15.416 19.934,15.416ZM22.688,11.78C22.688,12.164 22.598,12.578 22.418,13.022C22.238,13.466 21.962,13.88 21.59,14.264C21.242,14.588 20.912,14.804 20.6,14.912C20.372,14.972 20.066,15.02 19.682,15.056C19.694,14.168 19.925,13.397 20.375,12.743C20.825,12.089 21.578,11.642 22.634,11.402C22.658,11.486 22.673,11.558 22.679,11.618C22.685,11.678 22.688,11.732 22.688,11.78Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M28,19.055C28,14.0541 23.9706,10 19,10C14.0294,10 10,14.0541 10,19.055C10,23.5746 13.2912,27.3207 17.5938,28L17.5938,21.6725L15.3086,21.6725L15.3086,19.055L17.5938,19.055L17.5938,17.0601C17.5938,14.7907 18.9374,13.5371 20.9932,13.5371C21.9779,13.5371 23.0078,13.714 23.0078,13.714L23.0078,15.9423L21.8729,15.9423C20.7549,15.9423 20.4063,16.6403 20.4063,17.3564L20.4063,19.055L22.9023,19.055L22.5033,21.6725L20.4063,21.6725L20.4063,28C24.7088,27.3207 28,23.5746 28,19.055"
android:strokeWidth="1"
android:fillColor="#FFFFFE"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M18.9992,10C14.03,10 10,14.0294 10,19.0003C10,22.9766 12.5785,26.3497 16.1549,27.5398C16.6052,27.6226 16.7693,27.3447 16.7693,27.1061C16.7693,26.8928 16.7615,26.3265 16.7571,25.5756C14.2537,26.1193 13.7255,24.3689 13.7255,24.3689C13.3161,23.3291 12.7261,23.0523 12.7261,23.0523C11.9089,22.4943 12.7879,22.5054 12.7879,22.5054C13.6913,22.5689 14.1664,23.433 14.1664,23.433C14.9692,24.8082 16.2731,24.4109 16.7858,24.1805C16.8676,23.5993 17.1002,23.2026 17.3571,22.9777C15.3587,22.7507 13.2576,21.9783 13.2576,18.5295C13.2576,17.5472 13.6084,16.7433 14.1841,16.1146C14.0913,15.8869 13.7824,14.9714 14.2725,13.7327C14.2725,13.7327 15.0278,13.4907 16.7472,14.6554C17.4649,14.4554 18.2351,14.3559 19.0003,14.3521C19.7649,14.3559 20.5346,14.4554 21.2534,14.6554C22.9717,13.4907 23.7258,13.7327 23.7258,13.7327C24.217,14.9714 23.9082,15.8869 23.8159,16.1146C24.3927,16.7433 24.7408,17.5472 24.7408,18.5295C24.7408,21.9871 22.6363,22.7479 20.6318,22.9706C20.9545,23.2485 21.2423,23.7977 21.2423,24.6375C21.2423,25.8403 21.2313,26.811 21.2313,27.1061C21.2313,27.3469 21.3937,27.6271 21.8501,27.5392C25.4237,26.3464 28,22.9755 28,19.0003C28,14.0294 23.97,10 18.9992,10"
android:strokeWidth="1"
android:fillColor="#161514"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M1,0L37,0A1,1 0,0 1,38 1L38,37A1,1 0,0 1,37 38L1,38A1,1 0,0 1,0 37L0,1A1,1 0,0 1,1 0z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M27.64,19.2045C27.64,18.5664 27.5827,17.9527 27.4764,17.3636L19,17.3636L19,20.845L23.8436,20.845C23.635,21.97 23.0009,22.9232 22.0477,23.5614L22.0477,25.8195L24.9564,25.8195C26.6582,24.2527 27.64,21.9455 27.64,19.2045L27.64,19.2045Z"
android:strokeWidth="1"
android:fillColor="#4285F4"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M19,28C21.43,28 23.4673,27.1941 24.9564,25.8195L22.0477,23.5614C21.2418,24.1014 20.2109,24.4205 19,24.4205C16.6559,24.4205 14.6718,22.8373 13.9641,20.71L10.9573,20.71L10.9573,23.0418C12.4382,25.9832 15.4818,28 19,28L19,28Z"
android:strokeWidth="1"
android:fillColor="#34A853"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M13.9641,20.71C13.7841,20.17 13.6818,19.5932 13.6818,19C13.6818,18.4068 13.7841,17.83 13.9641,17.29L13.9641,14.9582L10.9573,14.9582C10.3477,16.1732 10,17.5477 10,19C10,20.4523 10.3477,21.8268 10.9573,23.0418L13.9641,20.71L13.9641,20.71Z"
android:strokeWidth="1"
android:fillColor="#FBBC05"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M19,13.5795C20.3214,13.5795 21.5077,14.0336 22.4405,14.9255L25.0218,12.3441C23.4632,10.8918 21.4259,10 19,10C15.4818,10 12.4382,12.0168 10.9573,14.9582L13.9641,17.29C14.6718,15.1627 16.6559,13.5795 19,13.5795L19,13.5795Z"
android:strokeWidth="1"
android:fillColor="#EA4335"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M28,13.7317C27.3377,14.0254 26.626,14.2239 25.879,14.3132C26.6415,13.8561 27.227,13.1324 27.5027,12.2701C26.7892,12.6932 25.9989,13.0006 25.1577,13.1662C24.484,12.4485 23.5243,12 22.4621,12C20.4226,12 18.7691,13.6534 18.7691,15.6928C18.7691,15.9823 18.8018,16.2641 18.8648,16.5344C15.7956,16.3804 13.0745,14.9102 11.2531,12.676C10.9352,13.2214 10.7531,13.8558 10.7531,14.5325C10.7531,15.8137 11.4051,16.9441 12.396,17.6063C11.7906,17.5871 11.2212,17.421 10.7233,17.1444C10.723,17.1598 10.723,17.1753 10.723,17.1908C10.723,18.9801 11.9959,20.4727 13.6853,20.8119C13.3754,20.8963 13.0492,20.9414 12.7123,20.9414C12.4744,20.9414 12.243,20.9183 12.0176,20.8752C12.4875,22.3423 13.8513,23.41 15.4673,23.4398C14.2034,24.4303 12.6111,25.0207 10.8809,25.0207C10.5829,25.0207 10.2889,25.0032 10,24.9691C11.6343,26.0169 13.5754,26.6282 15.6609,26.6282C22.4535,26.6282 26.1679,21.0011 26.1679,16.1211C26.1679,15.9609 26.1644,15.8017 26.1573,15.6433C26.8787,15.1227 27.5049,14.4722 28,13.7317"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -19,9 +19,9 @@
android:id="@+id/loginServerIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@drawable/ic_logo_matrix_org"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_logo_matrix_org" />
<TextView
android:id="@+id/loginTitle"
@ -95,8 +95,8 @@
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye"
tools:contentDescription="@string/a11y_show_password"
app:tint="?attr/colorAccent"
tools:contentDescription="@string/a11y_show_password"
tools:ignore="MissingPrefix" />
</FrameLayout>
@ -136,6 +136,37 @@
</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:padding="8dp"
android:clipToPadding="false"
android:clipChildren="false"
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
app:signMode="signin"
android:id="@+id/loginSocialLoginButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -53,14 +53,14 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginServerUrlFormHomeServerUrlTil"
style="@style/VectorTextInputLayout"
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">
<com.google.android.material.textfield.TextInputEditText
<AutoCompleteTextView
android:id="@+id/loginServerUrlFormHomeServerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -75,12 +75,45 @@
android:layout_marginTop="14dp"
android:text="@string/login_signin"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/loginSignupSigninSignInSocialLoginContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninSubmit"
tools:visibility="visible" />
<!-- Social Logins buttons -->
<LinearLayout
android:orientation="vertical"
android:id="@+id/loginSignupSigninSignInSocialLoginContainer"
app:layout_constraintTop_toBottomOf="@id/loginSignupSigninSignIn"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:clipToPadding="false"
android:clipChildren="false"
android:padding="8dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/loginSignupSigninSocialLoginHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
android:gravity="center"
android:textSize="14sp"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
android:text="@string/login_social_continue" />
<im.vector.app.features.login.SocialLoginButtonsView
app:signMode="continue_with"
android:id="@+id/loginSignupSigninSocialLoginButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -41,6 +41,12 @@
<attr name="vctr_icon_tint_on_light_action_bar_color" format="color" />
<attr name="vctr_settings_icon_tint_color" format="color" />
<attr name="vctr_social_login_button_google_style" format="reference" />
<attr name="vctr_social_login_button_github_style" format="reference" />
<attr name="vctr_social_login_button_facebook_style" format="reference" />
<attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_style" format="reference" />
</declare-styleable>
<declare-styleable name="PollResultLineView">
@ -66,4 +72,12 @@
<attr name="leftIcon" />
<attr name="textColor" format="color" />
</declare-styleable>
<declare-styleable name="SocialLoginButtonsView">
<attr name="signMode" format="enum">
<enum name="signin" value="0"/>
<enum name="signup" value="1"/>
<enum name="continue_with" value="2"/>
</attr>
</declare-styleable>
</resources>

View file

@ -41,6 +41,7 @@
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="black_alpha">#55000000</color>
<color name="black_54">#8A000000</color>
<!-- Palette: format fo naming:
'riotx_<name in the palette snake case>_<theme>'

View file

@ -1984,6 +1984,12 @@
<string name="login_server_other_title">Other</string>
<string name="login_server_other_text">Custom &amp; advanced settings</string>
<string name="login_social_continue">Or</string>
<string name="login_social_continue_with">Continue with %s</string>
<string name="login_social_signup_with">Sign up with %s</string>
<string name="login_social_signin_with">Sign in with %s</string>
<string name="login_continue">Continue</string>
<!-- Replaced string is the homeserver url -->
<string name="login_connect_to">Connect to %1$s</string>

View file

@ -370,5 +370,103 @@
<item name="android:background">@drawable/vector_tabbar_background</item>
<item name="background">@drawable/vector_tabbar_background</item>
</style>
<style name="WidgetButtonSocialLogin" parent="Widget.MaterialComponents.Button">
<item name="android:textAllCaps">false</item>
<item name="fontFamily">sans-serif-medium</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="iconGravity">start</item>
<item name="android:textSize">14sp</item>
<item name="android:textAlignment">textStart</item>
<item name="android:paddingStart">2dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:clipToPadding">false</item>
</style>
<style name="WidgetButtonSocialLogin.Google">
<item name="icon">@drawable/ic_social_google</item>
<item name="iconTint">@android:color/transparent</item>
<item name="iconTintMode">add</item>
</style>
<style name="WidgetButtonSocialLogin.Google.Light">
<item name="android:backgroundTint">@color/button_social_google_background_selector_light</item>
<item name="android:textColor">@color/black_54</item>
</style>
<style name="WidgetButtonSocialLogin.Google.Dark" >
<item name="android:backgroundTint">@color/button_social_google_background_selector_dark</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Github" parent="WidgetButtonSocialLogin">
<item name="icon">@drawable/ic_social_github</item>
</style>
<style name="WidgetButtonSocialLogin.Github.Light">
<item name="iconTint">@android:color/black</item>
<item name="android:textColor">@color/black</item>
<item name="android:backgroundTint">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Github.Dark" >
<item name="iconTint">@android:color/white</item>
<item name="android:textColor">@color/white</item>
<item name="android:backgroundTint">@color/black</item>
</style>
<style name="WidgetButtonSocialLogin.Facebook" parent="WidgetButtonSocialLogin">
<item name="icon">@drawable/ic_social_facebook</item>
</style>
<style name="WidgetButtonSocialLogin.Facebook.Light">
<item name="strokeColor">#3877EA</item>
<item name="strokeWidth">1dp</item>
<item name="iconTint">#3877EA</item>
<item name="android:textColor">#3877EA</item>
<item name="android:backgroundTint">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Facebook.Dark" >
<item name="iconTint">@android:color/white</item>
<item name="android:textColor">@color/white</item>
<item name="android:backgroundTint">#3877EA</item>
</style>
<style name="WidgetButtonSocialLogin.Twitter" parent="WidgetButtonSocialLogin">
<item name="icon">@drawable/ic_social_twitter</item>
</style>
<style name="WidgetButtonSocialLogin.Twitter.Light" >
<item name="iconTint">#5D9EC9</item>
<item name="android:textColor">#5D9EC9</item>
<item name="android:backgroundTint">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Twitter.Dark">
<item name="iconTint">@color/white</item>
<item name="android:textColor">@color/white</item>
<item name="android:backgroundTint">#5D9EC9</item>
</style>
<style name="WidgetButtonSocialLogin.Apple" parent="WidgetButtonSocialLogin">
<item name="icon">@drawable/ic_social_apple</item>
</style>
<style name="WidgetButtonSocialLogin.Apple.Light" >
<item name="iconTint">@color/white</item>
<item name="android:textColor">@color/white</item>
<item name="android:backgroundTint">@color/black</item>
</style>
<style name="WidgetButtonSocialLogin.Apple.Dark">
<item name="iconTint">@color/black</item>
<item name="android:textColor">@color/black</item>
<item name="android:backgroundTint">@color/white</item>
</style>
</resources>

View file

@ -194,6 +194,12 @@
<!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
<item name="vctr_social_login_button_google_style">@style/WidgetButtonSocialLogin.Google.Dark</item>
<item name="vctr_social_login_button_github_style">@style/WidgetButtonSocialLogin.Github.Dark</item>
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Dark</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Dark</item>
</style>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View file

@ -196,6 +196,13 @@
<!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
<item name="vctr_social_login_button_google_style">@style/WidgetButtonSocialLogin.Google.Light</item>
<item name="vctr_social_login_button_github_style">@style/WidgetButtonSocialLogin.Github.Light</item>
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Light</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Light</item>
</style>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" />