Merge pull request #2484 from vector-im/feature/bca/social_login

Social Login
This commit is contained in:
Benoit Marty 2020-12-14 18:19:23 +01:00 committed by GitHub
commit a027ef29e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1032 additions and 166 deletions

View file

@ -7,11 +7,13 @@ Features ✨:
- Url preview (#481)
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
- Emoji Keyboard (#2520)
- Social login (#2452)
Improvements 🙌:
- Add Setting Item to Change PIN (#2462)
- Improve room history visibility setting UX (#1579)
- Matrix.to deeplink custom scheme support
- Homeserver history (#1933)
Bugfix 🐛:
- Fix cancellation of sending event (#2438)

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

@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.auth
/**
* Path to use when the client does not supported any or all login flows
* Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback
* */
*/
const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/"
/**

View file

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

@ -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<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean

View file

@ -0,0 +1,52 @@
/*
* 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.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 SsoIdentityProvider(
/**
* 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 iconUrl: String?
) : Parcelable {
companion object {
// Not really defined by the spec, but we may define some ids here
const val ID_GOOGLE = "google"
const val ID_GITHUB = "github"
const val ID_APPLE = "apple"
const val ID_FACEBOOK = "facebook"
const val ID_TWITTER = "twitter"
}
}

View file

@ -36,7 +36,7 @@ sealed class CallState {
* Connected. Incoming/Outgoing call, ice layer connecting or connected
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
* could be exchanged, and the connection could go back to connected
* */
*/
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
/** Terminated. Incoming/Outgoing call, the call is terminated */

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.di.AuthDatabase
import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter
import org.matrix.android.sdk.internal.wellknown.WellknownModule
import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import java.io.File
@Module(includes = [WellknownModule::class])
@ -80,4 +81,7 @@ internal abstract class AuthModule {
@Binds
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
@Binds
abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService
}

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 }?.ssoIdentityProvider,
versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl,
!versions.isSupportedBySdk()

View file

@ -0,0 +1,50 @@
/*
* 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.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.SsoIdentityProvider
@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 ssoIdentityProvider: List<SsoIdentityProvider>?
)

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,41 @@
/*
* 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.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)
.addPrimaryKey(KnownServerUrlEntityFields.URL)
.setRequired(KnownServerUrlEntityFields.URL, true)
}
}

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

@ -57,6 +57,9 @@ internal abstract class RawModule {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-global.realm")
.schemaVersion(GlobalRealmMigration.SCHEMA_VERSION)
.migration(GlobalRealmMigration)
.allowWritesOnUiThread(true)
.modules(GlobalRealmModule())
.build()
}

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
@ -230,11 +229,6 @@ interface FragmentModule {
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSignUpSignInSsoFragment::class)
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSplashFragment::class)

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

@ -0,0 +1,93 @@
/*
* 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 com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab
abstract class AbstractSSOLoginFragment : AbstractLoginFragment() {
// 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)
}
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)
prefetchUrl(state.getSsoUrl(null))
}
}
}
}

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.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
@ -59,8 +60,13 @@ sealed class LoginAction : VectorViewModelAction {
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
// Homeserver history
object ClearHomeServerHistory : LoginAction()
// 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 ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginAction()
data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()

View file

@ -157,11 +157,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
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 ->
@ -252,7 +248,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
// 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,

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
@ -50,7 +51,7 @@ import javax.inject.Inject
* In signup mode:
* - the user is asked for login and password
*/
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
class LoginFragment @Inject constructor() : AbstractSSOLoginFragment() {
private var passwordShown = false
private var isSignupMode = false
@ -83,11 +84,13 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
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
}
@ -169,6 +172,19 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
ServerType.Unknown -> Unit /* Should not happen */
}
loginPasswordNotice.isVisible = false
if (state.loginMode is LoginMode.SsoAndPassword) {
loginSocialLoginContainer.isVisible = true
loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders
loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
openInCustomTab(state.getSsoUrl(id))
}
}
} else {
loginSocialLoginContainer.isVisible = false
loginSocialLoginButtons.ssoIdentityProviders = null
}
}
}

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.SsoIdentityProvider
sealed class LoginMode : Parcelable
/** because persist state */ {
@Parcelize object Unknown : LoginMode()
@Parcelize object Password : LoginMode()
@Parcelize data class Sso(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
@Parcelize object Unsupported : LoginMode()
}
fun LoginMode.ssoIdentityProviders() : List<SsoIdentityProvider>? {
return when (this) {
is LoginMode.Sso -> ssoIdentityProviders
is LoginMode.SsoAndPassword -> ssoIdentityProviders
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))
}
}
}

View file

@ -20,9 +20,13 @@ 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.isInvisible
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.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.utils.ensureProtocol
@ -55,6 +59,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
loginServerUrlFormHomeServerUrl.dismissDropDown()
submit()
return@setOnEditorActionListener true
}
@ -81,6 +86,15 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice)
}
}
val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList()
loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(
requireContext(),
R.layout.item_completion_homeserver,
completions
))
loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
.takeIf { completions.isNotEmpty() }
?: TextInputLayout.END_ICON_NONE
}
@OnClick(R.id.loginServerUrlFormLearnMore)
@ -88,6 +102,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
}
@OnClick(R.id.loginServerUrlFormClearHistory)
fun clearHistory() {
loginViewModel.handle(LoginAction.ClearHomeServerHistory)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetHomeServerUrl)
}
@ -105,7 +124,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))
}
}
@ -129,9 +148,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
override fun updateWithState(state: LoginViewState) {
setupUi(state)
loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
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))
}
}
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.login
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 kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
@ -26,11 +27,11 @@ 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() : AbstractSSOLoginFragment() {
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)
@ -38,29 +39,61 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.EMS -> {
ServerType.EMS -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
loginSignupSigninServerIcon.isVisible = true
loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular)
loginSignupSigninText.text = state.homeServerUrl.toReducedUrl()
}
ServerType.Other -> {
ServerType.Other -> {
loginSignupSigninServerIcon.isVisible = false
loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
}
ServerType.Unknown -> Unit /* Should not happen */
ServerType.Unknown -> Unit /* Should not happen */
}
when (state.loginMode) {
is LoginMode.SsoAndPassword -> {
loginSignupSigninSignInSocialLoginContainer.isVisible = true
loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()
loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
val url = withState(loginViewModel) { it.getSsoUrl(id) }
openInCustomTab(url)
}
}
}
else -> {
// SSO only is managed without container as well as No sso
loginSignupSigninSignInSocialLoginContainer.isVisible = false
loginSignupSigninSocialLoginButtons.ssoIdentityProviders = null
}
}
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
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) {
openInCustomTab(state.getSsoUrl(null))
} else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
Unit
}
@OnClick(R.id.loginSignupSigninSignIn)
@ -74,6 +107,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

@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents {
object OpenServerSelection : LoginViewEvents()
data class OnServerSelectionDone(val serverType: ServerType) : LoginViewEvents()
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnLoginFlowRetrieved : LoginViewEvents()
data class OnSignModeSelected(val signMode: SignMode) : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : LoginViewEvents()

View file

@ -38,6 +38,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 +64,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,6 +73,16 @@ class LoginViewModel @AssistedInject constructor(
fun create(initialState: LoginViewState): LoginViewModel
}
init {
getKnownCustomHomeServersUrls()
}
private fun getKnownCustomHomeServersUrls() {
setState {
copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls())
}
}
companion object : MvRxViewModelFactory<LoginViewModel, LoginViewState> {
@JvmStatic
@ -121,6 +133,7 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
LoginAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
}.exhaustive
}
@ -129,10 +142,11 @@ 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 -> {
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
}
is LoginAction.LoginOrRegister ->
handleDirectLogin(
finalLastAction,
@ -145,6 +159,16 @@ class LoginViewModel @AssistedInject constructor(
}
}
private fun rememberHomeServer(homeServerUrl: String) {
homeServerHistoryService.addHomeServerToHistory(homeServerUrl)
getKnownCustomHomeServersUrls()
}
private fun handleClearHomeServerHistory() {
homeServerHistoryService.clearHistory()
getKnownCustomHomeServersUrls()
}
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
val safeLoginWizard = loginWizard
@ -184,7 +208,7 @@ class LoginViewModel @AssistedInject constructor(
setState {
copy(
signMode = SignMode.SignIn,
loginMode = LoginMode.Sso,
loginMode = LoginMode.Sso(action.ssoIdentityProviders),
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
)
@ -713,7 +737,6 @@ 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")))
@ -751,13 +774,19 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onSuccess(data: LoginFlowResult) {
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, data.homeServerUrl can be different
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
when (data) {
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,157 @@
/*
* 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.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.SsoIdentityProvider
class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
interface InteractionListener {
fun onProviderSelected(id: String?)
}
enum class Mode {
MODE_SIGN_IN,
MODE_SIGN_UP,
MODE_CONTINUE,
}
var ssoIdentityProviders: List<SsoIdentityProvider>? = null
set(newProviders) {
if (newProviders != ssoIdentityProviders) {
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 (ssoIdentityProviders.isNullOrEmpty()) {
// Put a default sign in with sso button
MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply {
transformationMethod = null
textAlignment = View.TEXT_ALIGNMENT_CENTER
}.let {
it.text = getButtonTitle(context.getString(R.string.login_social_sso))
it.textAlignment = View.TEXT_ALIGNMENT_CENTER
it.setOnClickListener {
listener?.onProviderSelected(null)
}
addView(it)
}
return
}
ssoIdentityProviders?.forEach { identityProvider ->
// Use some heuristic to render buttons according to branding guidelines
val button: MaterialButton = cachedViews[identityProvider.id]
?: when (identityProvider.id) {
SsoIdentityProvider.ID_GOOGLE -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_google_style)
}
SsoIdentityProvider.ID_GITHUB -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_github_style)
}
SsoIdentityProvider.ID_APPLE -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style)
}
SsoIdentityProvider.ID_FACEBOOK -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style)
}
SsoIdentityProvider.ID_TWITTER -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style)
}
else -> {
// TODO Use iconUrl
MaterialButton(context, null, R.attr.materialButtonStyle).apply {
transformationMethod = null
textAlignment = View.TEXT_ALIGNMENT_CENTER
}
}
}
button.text = getButtonTitle(identityProvider.name)
button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id)
button.setOnClickListener {
listener?.onProviderSelected(identityProvider.id)
}
addView(button)
}
}
private fun getButtonTitle(providerName: String?): String {
return when (mode) {
Mode.MODE_SIGN_IN -> context.getString(R.string.login_social_signin_with, providerName)
Mode.MODE_SIGN_UP -> context.getString(R.string.login_social_signup_with, providerName)
Mode.MODE_CONTINUE -> context.getString(R.string.login_social_continue_with, providerName)
}
}
init {
this.orientation = VERTICAL
gravity = Gravity.CENTER
clipToPadding = false
clipChildren = false
if (isInEditMode) {
ssoIdentityProviders = listOf(
SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null),
SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null),
SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null),
SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null),
SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null),
SsoIdentityProvider("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()
update()
}
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.ssoIdentityProviders
))
}
is LoginMode.Sso -> {
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId,
mode.ssoIdentityProviders
))
}
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,35 @@
</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: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>

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"
@ -70,6 +70,17 @@
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/loginServerUrlFormClearHistory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/login_clear_homeserver_history"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textColor="@color/riotx_accent"
android:visibility="invisible"
tools:visibility="visible" />
<TextView
android:id="@+id/loginServerUrlFormNotice"
android:layout_width="wrap_content"

View file

@ -26,10 +26,10 @@
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginLogo"
tools:src="@drawable/ic_logo_matrix_org"
tools:visibility="visible"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix,UnknownId" />
tools:ignore="MissingPrefix,UnknownId"
tools:src="@drawable/ic_logo_matrix_org"
tools:visibility="visible" />
<TextView
android:id="@+id/loginSignupSigninTitle"
@ -75,12 +75,43 @@
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:id="@+id/loginSignupSigninSignInSocialLoginContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginSignupSigninSignIn">
<TextView
android:id="@+id/loginSignupSigninSocialLoginHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
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/loginSignupSigninSocialLoginButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:signMode="continue_with" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:textSize="14sp"
tools:text="https://matrix.org" />

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,13 @@
<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_social_sso">single sign-on</string>
<string name="login_continue">Continue</string>
<!-- Replaced string is the homeserver url -->
<string name="login_connect_to">Connect to %1$s</string>
@ -1994,6 +2001,7 @@
<string name="login_signup">Sign Up</string>
<string name="login_signin">Sign In</string>
<string name="login_signin_sso">Continue with SSO</string>
<string name="login_clear_homeserver_history">Clear history</string>
<string name="login_server_url_form_modular_hint">Element Matrix Services Address</string>
<string name="login_server_url_form_other_hint">Address</string>

View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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" />