diff --git a/changelog.d/6827.bugfix b/changelog.d/6827.bugfix new file mode 100644 index 0000000000..2c3e130aa3 --- /dev/null +++ b/changelog.d/6827.bugfix @@ -0,0 +1 @@ +Fixing sign in/up for homeservers that rely on the SSO fallback url diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index b18df6c9cf..ddab65d981 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -85,7 +85,7 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragmen private fun prefetchIfNeeded() { withState(loginViewModel) { state -> - if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { + if (state.loginMode.hasSso() && state.loginMode.ssoState().isFallback()) { // in this case we can prefetch (not other cases for privacy concerns) loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 9c598c400b..03010e0a75 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -100,13 +99,11 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment error("developer error") - SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP - SignMode.SignIn, - SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN - } + private fun ssoMode(state: LoginViewState) = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP + SignMode.SignIn, + SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN } private fun submit() { @@ -201,16 +198,13 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment + loginViewModel.getSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = provider?.id + ) + ?.let { openInCustomTab(it) } } } else { views.loginSocialLoginContainer.isVisible = false @@ -272,7 +266,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment?) : LoginMode() - @Parcelize data class SsoAndPassword(val ssoIdentityProviders: List?) : LoginMode() + @Parcelize data class Sso(val ssoState: SsoState) : LoginMode() + @Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode() @Parcelize object Unsupported : LoginMode() } -fun LoginMode.ssoIdentityProviders(): List? { +fun LoginMode.ssoState(): SsoState { return when (this) { - is LoginMode.Sso -> ssoIdentityProviders - is LoginMode.SsoAndPassword -> ssoIdentityProviders - else -> null + is LoginMode.Sso -> ssoState + is LoginMode.SsoAndPassword -> ssoState + else -> SsoState.Fallback } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt index 1325ea37af..aafd426335 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt @@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import im.vector.app.features.login.SocialLoginButtonsView.Mode import javax.inject.Inject /** @@ -73,16 +73,13 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi when (state.loginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted() - views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { - override fun onProviderSelected(provider: SsoIdentityProvider?) { - loginViewModel.getSsoUrl( - redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, - deviceId = state.deviceId, - providerId = provider?.id - ) - ?.let { openInCustomTab(it) } - } + views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider -> + loginViewModel.getSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = provider?.id + ) + ?.let { openInCustomTab(it) } } } else -> { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 40f72ccc99..79d06a0864 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -223,7 +223,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy( signMode = SignMode.SignIn, - loginMode = LoginMode.Sso(action.ssoIdentityProviders), + loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()), homeServerUrlFromUser = action.homeServerUrl, homeServerUrl = action.homeServerUrl, deviceId = action.deviceId @@ -816,8 +816,8 @@ class LoginViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first 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.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 4f3b1237f2..816050420e 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -160,8 +160,11 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: } } -fun SocialLoginButtonsView.render(ssoProviders: List?, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { +fun SocialLoginButtonsView.render(state: SsoState, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { this.mode = mode - this.ssoIdentityProviders = ssoProviders?.sorted() + this.ssoIdentityProviders = when (state) { + SsoState.Fallback -> null + is SsoState.IdentityProviders -> state.providers.sorted() + } this.listener = SocialLoginButtonsView.InteractionListener { listener(it) } } diff --git a/vector/src/main/java/im/vector/app/features/login/SsoState.kt b/vector/src/main/java/im/vector/app/features/login/SsoState.kt new file mode 100644 index 0000000000..5f57780bd7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/SsoState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 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.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider + +sealed interface SsoState : Parcelable { + @Parcelize + data class IdentityProviders(val providers: List) : SsoState + + @Parcelize + object Fallback : SsoState + + fun isFallback() = this == Fallback + + fun providersOrNull() = when (this) { + Fallback -> null + is IdentityProviders -> providers.takeIf { it.isNotEmpty() } + } +} + +fun List?.toSsoState() = this + ?.takeIf { it.isNotEmpty() } + ?.let { SsoState.IdentityProviders(it) } + ?: SsoState.Fallback diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index 2dc9a05154..db21a53854 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.onboarding import im.vector.app.core.extensions.containsAllItems import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.toSsoState import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -50,8 +51,8 @@ class StartAuthenticationFlowUseCase @Inject constructor( ) private fun LoginFlowResult.findPreferredLoginMode() = when { - supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders) - supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders) + supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState()) + supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState()) supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt index 1b764f4ce6..b1352db0cc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt @@ -26,7 +26,7 @@ import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.hasSso -import im.vector.app.features.login.ssoIdentityProviders +import im.vector.app.features.login.ssoState abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthFragment() { @@ -88,7 +88,7 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF private fun prefetchIfNeeded() { withState(viewModel) { state -> - if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders().isNullOrEmpty()) { + if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoState().isFallback()) { // in this case we can prefetch (not other cases for privacy concerns) viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index c2d2346765..b24511b4bc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -38,13 +38,13 @@ import im.vector.app.databinding.FragmentFtueCombinedLoginBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView +import im.vector.app.features.login.SsoState import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject @@ -125,11 +125,11 @@ class FtueAuthCombinedLoginFragment @Inject constructor( when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { showUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) } is LoginMode.Sso -> { hideUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) } else -> { showUsernamePassword() @@ -138,10 +138,10 @@ class FtueAuthCombinedLoginFragment @Inject constructor( } } - private fun renderSsoProviders(deviceId: String?, ssoProviders: List?) { - views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true - views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible - views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> + private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + views.ssoGroup.isVisible = true + views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible() + views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, @@ -163,6 +163,8 @@ class FtueAuthCombinedLoginFragment @Inject constructor( views.loginEntryGroup.isVisible = true } + private fun isUsernameAndPasswordVisible() = views.loginEntryGroup.isVisible + private fun setupAutoFill() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 8340fb903a..d06d1e8051 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -44,6 +44,7 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView +import im.vector.app.features.login.SsoState import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction @@ -51,7 +52,6 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername @@ -205,14 +205,14 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu } when (state.selectedHomeserver.preferredLoginMode) { - is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) + is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) else -> hideSsoProviders() } } - private fun renderSsoProviders(deviceId: String?, ssoProviders: List?) { - views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true - views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> + private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + views.ssoGroup.isVisible = true + views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index 17ceb5314c..ad159943b1 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -37,7 +37,8 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode -import im.vector.app.features.login.SocialLoginButtonsView +import im.vector.app.features.login.SocialLoginButtonsView.Mode +import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isLoginEmailUnknown @@ -111,13 +111,11 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< } } - private fun setupSocialLoginButtons(state: OnboardingViewState) { - views.loginSocialLoginButtons.mode = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP - SignMode.SignIn, - SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN - } + private fun ssoMode(state: OnboardingViewState) = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> Mode.MODE_SIGN_UP + SignMode.SignIn, + SignMode.SignInWithMatrixId -> Mode.MODE_SIGN_IN } private fun submit() { @@ -215,16 +213,13 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders?.sorted() - views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { - override fun onProviderSelected(provider: SsoIdentityProvider?) { - viewModel.fetchSsoUrl( - redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, - deviceId = state.deviceId, - provider = provider - ) - ?.let { openInCustomTab(it) } - } + views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, ssoMode(state)) { provider -> + viewModel.fetchSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + provider = provider + ) + ?.let { openInCustomTab(it) } } } else { views.loginSocialLoginContainer.isVisible = false @@ -305,7 +300,6 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< setupUi(state) setupAutoFill(state) - setupSocialLoginButtons(state) setupButtons(state) if (state.isLoading) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt index 6723e48bcc..dc4c6d8a34 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt @@ -30,11 +30,10 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode -import im.vector.app.features.login.SocialLoginButtonsView -import im.vector.app.features.login.ssoIdentityProviders +import im.vector.app.features.login.SocialLoginButtonsView.Mode +import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState -import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import javax.inject.Inject /** @@ -80,16 +79,13 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders()?.sorted() - views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { - override fun onProviderSelected(provider: SsoIdentityProvider?) { - viewModel.fetchSsoUrl( - redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, - deviceId = state.deviceId, - provider = provider - ) - ?.let { openInCustomTab(it) } - } + views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, Mode.MODE_CONTINUE) { provider -> + viewModel.fetchSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + provider = provider + ) + ?.let { openInCustomTab(it) } } } else -> { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 5b369d4b49..483e3a140f 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -63,7 +63,7 @@ class SoftLogoutFragment @Inject constructor( LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, softLogoutViewState.deviceId, - mode.ssoIdentityProviders + mode.ssoState.providersOrNull() ) ) } @@ -72,7 +72,7 @@ class SoftLogoutFragment @Inject constructor( LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, softLogoutViewState.deviceId, - mode.ssoIdentityProviders + mode.ssoState.providersOrNull() ) ) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 9d0580638b..f3e2f82edc 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -33,6 +33,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.hasUnsavedKeys import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.toSsoState import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.LoginType @@ -115,8 +116,8 @@ class SoftLogoutViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first 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.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt index 48cd32c84d..d15a6cf042 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt @@ -17,6 +17,7 @@ package im.vector.app.features.onboarding import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SsoState import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeUri @@ -32,7 +33,11 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider private const val A_DECLARED_HOMESERVER_URL = "https://foo.bar" private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(homeServerUri = FakeUri().instance) -private val SSO_IDENTITY_PROVIDERS = emptyList() +private val FALLBACK_SSO_IDENTITY_PROVIDERS = emptyList() +private val SSO_IDENTITY_PROVIDERS = listOf(SsoIdentityProvider(id = "id", "name", null, "sso-brand")) +private val SSO_LOGIN_TYPE = listOf(LoginFlowTypes.SSO) +private val SSO_AND_PASSWORD_LOGIN_TYPES = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) +private val PASSWORD_LOGIN_TYPE = listOf(LoginFlowTypes.PASSWORD) class StartAuthenticationFlowUseCaseTest { @@ -47,7 +52,7 @@ class StartAuthenticationFlowUseCaseTest { @Test fun `given empty login result when starting authentication flow then returns empty result`() = runTest { - val loginResult = aLoginResult() + val loginResult = aLoginResult(supportedLoginTypes = emptyList()) fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) val result = useCase.execute(A_HOMESERVER_CONFIG) @@ -57,55 +62,81 @@ class StartAuthenticationFlowUseCaseTest { } @Test - fun `given login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest { - val supportedLoginTypes = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) - val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes) + fun `given empty sso providers and login supports SSO and Password when starting authentication flow then prefers fallback SsoAndPassword`() = runTest { + val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = emptyList()) fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) val result = useCase.execute(A_HOMESERVER_CONFIG) result shouldBeEqualTo expectedResult( - supportedLoginTypes = supportedLoginTypes, - preferredLoginMode = LoginMode.SsoAndPassword(SSO_IDENTITY_PROVIDERS), + supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, + preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @Test - fun `given login supports SSO when starting authentication flow then prefers Sso`() = runTest { - val supportedLoginTypes = listOf(LoginFlowTypes.SSO) - val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes) + fun `given sso providers and login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest { + val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = SSO_IDENTITY_PROVIDERS) fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) val result = useCase.execute(A_HOMESERVER_CONFIG) result shouldBeEqualTo expectedResult( - supportedLoginTypes = supportedLoginTypes, - preferredLoginMode = LoginMode.Sso(SSO_IDENTITY_PROVIDERS), + supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, + preferredLoginMode = LoginMode.SsoAndPassword(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)), + ) + verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) + } + + @Test + fun `given empty sso providers and login supports SSO when starting authentication flow then prefers fallback Sso`() = runTest { + val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = emptyList()) + fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) + + val result = useCase.execute(A_HOMESERVER_CONFIG) + + result shouldBeEqualTo expectedResult( + supportedLoginTypes = SSO_LOGIN_TYPE, + preferredLoginMode = LoginMode.Sso(SsoState.Fallback), + ) + verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) + } + + @Test + fun `given identity providers and login supports SSO when starting authentication flow then prefers Sso`() = runTest { + val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = SSO_IDENTITY_PROVIDERS) + fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) + + val result = useCase.execute(A_HOMESERVER_CONFIG) + + result shouldBeEqualTo expectedResult( + supportedLoginTypes = SSO_LOGIN_TYPE, + preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @Test fun `given login supports Password when starting authentication flow then prefers Password`() = runTest { - val supportedLoginTypes = listOf(LoginFlowTypes.PASSWORD) - val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes) + val loginResult = aLoginResult(supportedLoginTypes = PASSWORD_LOGIN_TYPE) fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) val result = useCase.execute(A_HOMESERVER_CONFIG) result shouldBeEqualTo expectedResult( - supportedLoginTypes = supportedLoginTypes, + supportedLoginTypes = PASSWORD_LOGIN_TYPE, preferredLoginMode = LoginMode.Password, ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } private fun aLoginResult( - supportedLoginTypes: List = emptyList() + supportedLoginTypes: List, + ssoProviders: List = FALLBACK_SSO_IDENTITY_PROVIDERS ) = LoginFlowResult( supportedLoginTypes = supportedLoginTypes, - ssoIdentityProviders = SSO_IDENTITY_PROVIDERS, + ssoIdentityProviders = ssoProviders, isLoginAndRegistrationSupported = true, homeServerUrl = A_DECLARED_HOMESERVER_URL, isOutdatedHomeserver = false,