diff --git a/changelog.d/5325.feature b/changelog.d/5325.feature new file mode 100644 index 0000000000..23754c790d --- /dev/null +++ b/changelog.d/5325.feature @@ -0,0 +1 @@ +Adds forceLoginFallback feature flag and usages to FTUE login and registration \ No newline at end of file diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 5cc4bd3bde..8702c8d966 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor( label = "FTUE Personalize profile", key = DebugFeatureKeys.onboardingPersonalize, factory = VectorFeatures::isOnboardingPersonalizeEnabled - ) + ), )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt index 808c379354..b54d776901 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt @@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked)) } + views.forceLoginFallback.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled(isChecked)) + } } override fun invalidate() = withState(viewModel) { views.forceDialPadTabDisplay.isChecked = it.dialPadVisible + views.forceLoginFallback.isChecked = it.forceLoginFallback } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt index ecbb241387..1c76cf6fb2 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class DebugPrivateSettingsViewActions : VectorViewModelAction { data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions() + data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions() } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt index 624c46556a..038b1e6cc7 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt @@ -45,15 +45,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( private fun observeVectorDataStore() { vectorDataStore.forceDialPadDisplayFlow.setOnEach { - copy( - dialPadVisible = it - ) + copy(dialPadVisible = it) + } + + vectorDataStore.forceLoginFallbackFlow.setOnEach { + copy(forceLoginFallback = it) } } override fun handle(action: DebugPrivateSettingsViewActions) { when (action) { - is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action) + is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action) + is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action) } } @@ -62,4 +65,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( vectorDataStore.setForceDialPadDisplay(action.force) } } + + private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) { + viewModelScope.launch { + vectorDataStore.setForceLoginFallbackFlow(action.force) + } + } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt index 0ad4b185ec..7fca29af8c 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt @@ -19,5 +19,6 @@ package im.vector.app.features.debug.settings import com.airbnb.mvrx.MavericksState data class DebugPrivateSettingsViewState( - val dialPadVisible: Boolean = false + val dialPadVisible: Boolean = false, + val forceLoginFallback: Boolean = false, ) : MavericksState diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector/src/debug/res/layout/fragment_debug_private_settings.xml index b4186e7bba..6760c68169 100644 --- a/vector/src/debug/res/layout/fragment_debug_private_settings.xml +++ b/vector/src/debug/res/layout/fragment_debug_private_settings.xml @@ -25,6 +25,12 @@ android:layout_height="wrap_content" android:text="Force DialPad tab display" /> + + diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index ca3c3644bd..63f1875235 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.settings.VectorDataStore import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain @@ -78,7 +79,8 @@ class OnboardingViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val homeServerHistoryService: HomeServerHistoryService, private val vectorFeatures: VectorFeatures, - private val analyticsTracker: AnalyticsTracker + private val analyticsTracker: AnalyticsTracker, + private val vectorDataStore: VectorDataStore, ) : VectorViewModel(initialState) { @AssistedFactory @@ -90,6 +92,7 @@ class OnboardingViewModel @AssistedInject constructor( init { getKnownCustomHomeServersUrls() + observeDataStore() } private fun getKnownCustomHomeServersUrls() { @@ -98,6 +101,12 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun observeDataStore() = viewModelScope.launch { + vectorDataStore.forceLoginFallbackFlow.setOnEach { isForceLoginFallbackEnabled -> + copy(isForceLoginFallbackEnabled = isForceLoginFallbackEnabled) + } + } + // Store the last action, to redo it after user has trusted the untrusted certificate private var lastAction: OnboardingAction? = null private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index 7bad2682a9..39c5094d30 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -62,7 +62,8 @@ data class OnboardingViewState( // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable @PersistState val loginModeSupportedTypes: List = emptyList(), - val knownCustomHomeServersUrls: List = emptyList() + val knownCustomHomeServersUrls: List = emptyList(), + val isForceLoginFallbackEnabled: Boolean = false, ) : MavericksState { fun isLoading(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 1e792df427..0093cb20ea 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -75,6 +75,8 @@ class FtueAuthVariant( private val popEnterAnim = R.anim.no_anim private val popExitAnim = R.anim.exit_fade_out + private var isForceLoginFallbackEnabled = false + private val topFragment: Fragment? get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id) @@ -109,10 +111,6 @@ class FtueAuthVariant( } } - override fun setIsLoading(isLoading: Boolean) { - // do nothing - } - private fun addFirstFragment() { val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) { true -> FtueAuthSplashCarouselFragment::class.java @@ -121,11 +119,25 @@ class FtueAuthVariant( activity.addFragment(views.loginFragmentContainer, splashFragment) } + private fun updateWithState(viewState: OnboardingViewState) { + isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled + views.loginLoading.isVisible = shouldShowLoading(viewState) + } + + private fun shouldShowLoading(viewState: OnboardingViewState) = + if (vectorFeatures.isOnboardingPersonalizeEnabled()) { + viewState.isLoading() + } else { + // Keep loading when during success because of the delay when switching to the next Activity + viewState.isLoading() || viewState.isAuthTaskCompleted() + } + + override fun setIsLoading(isLoading: Boolean) = Unit + private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) { when (viewEvents) { is OnboardingViewEvents.RegistrationFlowResult -> { - // Check that all flows are supported by the application - if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) { + if (registrationShouldFallback(viewEvents)) { // Display a popup to propose use web fallback onRegistrationStageNotSupported() } else { @@ -136,11 +148,7 @@ class FtueAuthVariant( // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. // This way it will be automatically popped in when starting the next registration stage - activity.addFragmentToBackstack(views.loginFragmentContainer, - FtueAuthLoginFragment::class.java, - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption - ) + openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) } } } @@ -228,13 +236,23 @@ class FtueAuthVariant( }.exhaustive } - private fun updateWithState(viewState: OnboardingViewState) { - views.loginLoading.isVisible = if (vectorFeatures.isOnboardingPersonalizeEnabled()) { - viewState.isLoading() - } else { - // Keep loading when during success because of the delay when switching to the next Activity - viewState.isLoading() || viewState.isAuthTaskCompleted() - } + private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) = + isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow() + + private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() = + flowResult.missingStages.any { !it.isSupported() } + + private fun onRegistrationStageNotSupported() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + FtueAuthWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() } private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.OnWebLoginError) { @@ -264,29 +282,58 @@ class FtueAuthVariant( // state.signMode could not be ready yet. So use value from the ViewEvent when (OnboardingViewEvents.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { - // This is managed by the OnboardingViewEvents - } - SignMode.SignIn -> { - // It depends on the LoginMode - when (state.loginMode) { - LoginMode.Unknown, - is LoginMode.Sso -> error("Developer error") - is LoginMode.SsoAndPassword, - LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer, - FtueAuthLoginFragment::class.java, - tag = FRAGMENT_LOGIN_TAG, - option = commonOption) - LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) - }.exhaustive - } - SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer, - FtueAuthLoginFragment::class.java, - tag = FRAGMENT_LOGIN_TAG, - option = commonOption) + SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents + SignMode.SignIn -> handleSignInSelected(state) + SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state) }.exhaustive } + private fun handleSignInSelected(state: OnboardingViewState) { + if (isForceLoginFallbackEnabled) { + onLoginModeNotSupported(state.loginModeSupportedTypes) + } else { + disambiguateLoginMode(state) + } + } + + private fun disambiguateLoginMode(state: OnboardingViewState) = when (state.loginMode) { + LoginMode.Unknown, + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, + LoginMode.Password -> openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG) + LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) + } + + private fun openAuthLoginFragmentWithTag(tag: String) { + activity.addFragmentToBackstack(views.loginFragmentContainer, + FtueAuthLoginFragment::class.java, + tag = tag, + option = commonOption) + } + + private fun onLoginModeNotSupported(supportedTypes: List) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> openAuthWebFragment() } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun handleSignInWithMatrixId(state: OnboardingViewState) { + if (isForceLoginFallbackEnabled) { + onLoginModeNotSupported(state.loginModeSupportedTypes) + } else { + openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG) + } + } + + private fun openAuthWebFragment() { + activity.addFragmentToBackstack(views.loginFragmentContainer, + FtueAuthWebFragment::class.java, + option = commonOption) + } + /** * Handle the SSO redirection here */ @@ -296,32 +343,6 @@ class FtueAuthVariant( ?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) } } - private fun onRegistrationStageNotSupported() { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.app_name) - .setMessage(activity.getString(R.string.login_registration_not_supported)) - .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(views.loginFragmentContainer, - FtueAuthWebFragment::class.java, - option = commonOption) - } - .setNegativeButton(R.string.no, null) - .show() - } - - private fun onLoginModeNotSupported(supportedTypes: List) { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.app_name) - .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) - .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(views.loginFragmentContainer, - FtueAuthWebFragment::class.java, - option = commonOption) - } - .setNegativeButton(R.string.no, null) - .show() - } - private fun handleRegistrationNavigation(flowResult: FlowResult) { // Complete all mandatory stages first val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt index 6a5ef0ac99..a7981a8b2a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt @@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor( settings[forceDialPadDisplay] = force } } + + private val forceLoginFallback = booleanPreferencesKey("force_login_fallback") + + val forceLoginFallbackFlow: Flow = context.dataStore.data.map { preferences -> + preferences[forceLoginFallback].orFalse() + } + + suspend fun setForceLoginFallbackFlow(force: Boolean) { + context.dataStore.edit { settings -> + settings[forceLoginFallback] = force + } + } }