Merge pull request #5325 from vector-im/feature/eric/registration-feature-flag

#5307 Adds ForceLoginFallback feature flag to Login and Registration
This commit is contained in:
Benoit Marty 2022-02-28 14:20:00 +01:00 committed by GitHub
commit 27905064e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 72 deletions

1
changelog.d/5325.feature Normal file
View file

@ -0,0 +1 @@
Adds forceLoginFallback feature flag and usages to FTUE login and registration

View file

@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor(
label = "FTUE Personalize profile", label = "FTUE Personalize profile",
key = DebugFeatureKeys.onboardingPersonalize, key = DebugFeatureKeys.onboardingPersonalize,
factory = VectorFeatures::isOnboardingPersonalizeEnabled factory = VectorFeatures::isOnboardingPersonalizeEnabled
) ),
)) ))
} }

View file

@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSett
views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked -> views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked)) viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
} }
views.forceLoginFallback.setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled(isChecked))
}
} }
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
views.forceLoginFallback.isChecked = it.forceLoginFallback
} }
} }

View file

@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction { sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions() data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
} }

View file

@ -45,15 +45,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
private fun observeVectorDataStore() { private fun observeVectorDataStore() {
vectorDataStore.forceDialPadDisplayFlow.setOnEach { vectorDataStore.forceDialPadDisplayFlow.setOnEach {
copy( copy(dialPadVisible = it)
dialPadVisible = it }
)
vectorDataStore.forceLoginFallbackFlow.setOnEach {
copy(forceLoginFallback = it)
} }
} }
override fun handle(action: DebugPrivateSettingsViewActions) { override fun handle(action: DebugPrivateSettingsViewActions) {
when (action) { 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) vectorDataStore.setForceDialPadDisplay(action.force)
} }
} }
private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) {
viewModelScope.launch {
vectorDataStore.setForceLoginFallbackFlow(action.force)
}
}
} }

View file

@ -19,5 +19,6 @@ package im.vector.app.features.debug.settings
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
data class DebugPrivateSettingsViewState( data class DebugPrivateSettingsViewState(
val dialPadVisible: Boolean = false val dialPadVisible: Boolean = false,
val forceLoginFallback: Boolean = false,
) : MavericksState ) : MavericksState

View file

@ -25,6 +25,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Force DialPad tab display" /> android:text="Force DialPad tab display" />
<CheckBox
android:id="@+id/forceLoginFallback"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Force login and registration fallback" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View file

@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import im.vector.app.features.settings.VectorDataStore
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.MatrixPatterns.getDomain
@ -78,7 +79,8 @@ class OnboardingViewModel @AssistedInject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService, private val homeServerHistoryService: HomeServerHistoryService,
private val vectorFeatures: VectorFeatures, private val vectorFeatures: VectorFeatures,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker,
private val vectorDataStore: VectorDataStore,
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -90,6 +92,7 @@ class OnboardingViewModel @AssistedInject constructor(
init { init {
getKnownCustomHomeServersUrls() getKnownCustomHomeServersUrls()
observeDataStore()
} }
private fun getKnownCustomHomeServersUrls() { 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 // Store the last action, to redo it after user has trusted the untrusted certificate
private var lastAction: OnboardingAction? = null private var lastAction: OnboardingAction? = null
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null

View file

@ -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 // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
@PersistState @PersistState
val loginModeSupportedTypes: List<String> = emptyList(), val loginModeSupportedTypes: List<String> = emptyList(),
val knownCustomHomeServersUrls: List<String> = emptyList() val knownCustomHomeServersUrls: List<String> = emptyList(),
val isForceLoginFallbackEnabled: Boolean = false,
) : MavericksState { ) : MavericksState {
fun isLoading(): Boolean { fun isLoading(): Boolean {

View file

@ -75,6 +75,8 @@ class FtueAuthVariant(
private val popEnterAnim = R.anim.no_anim private val popEnterAnim = R.anim.no_anim
private val popExitAnim = R.anim.exit_fade_out private val popExitAnim = R.anim.exit_fade_out
private var isForceLoginFallbackEnabled = false
private val topFragment: Fragment? private val topFragment: Fragment?
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id) get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
@ -109,10 +111,6 @@ class FtueAuthVariant(
} }
} }
override fun setIsLoading(isLoading: Boolean) {
// do nothing
}
private fun addFirstFragment() { private fun addFirstFragment() {
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) { val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
true -> FtueAuthSplashCarouselFragment::class.java true -> FtueAuthSplashCarouselFragment::class.java
@ -121,11 +119,25 @@ class FtueAuthVariant(
activity.addFragment(views.loginFragmentContainer, splashFragment) 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) { private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
when (viewEvents) { when (viewEvents) {
is OnboardingViewEvents.RegistrationFlowResult -> { is OnboardingViewEvents.RegistrationFlowResult -> {
// Check that all flows are supported by the application if (registrationShouldFallback(viewEvents)) {
if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) {
// Display a popup to propose use web fallback // Display a popup to propose use web fallback
onRegistrationStageNotSupported() onRegistrationStageNotSupported()
} else { } else {
@ -136,11 +148,7 @@ class FtueAuthVariant(
// First ask for login and password // First ask for login and password
// I add a tag to indicate that this fragment is a registration stage. // 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 // This way it will be automatically popped in when starting the next registration stage
activity.addFragmentToBackstack(views.loginFragmentContainer, openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
FtueAuthLoginFragment::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption
)
} }
} }
} }
@ -228,13 +236,23 @@ class FtueAuthVariant(
}.exhaustive }.exhaustive
} }
private fun updateWithState(viewState: OnboardingViewState) { private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
views.loginLoading.isVisible = if (vectorFeatures.isOnboardingPersonalizeEnabled()) { isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow()
viewState.isLoading()
} else { private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() =
// Keep loading when during success because of the delay when switching to the next Activity flowResult.missingStages.any { !it.isSupported() }
viewState.isLoading() || viewState.isAuthTaskCompleted()
} 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) { 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 // state.signMode could not be ready yet. So use value from the ViewEvent
when (OnboardingViewEvents.signMode) { when (OnboardingViewEvents.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> { SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents
// This is managed by the OnboardingViewEvents SignMode.SignIn -> handleSignInSelected(state)
} SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state)
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)
}.exhaustive }.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<String>) {
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 * Handle the SSO redirection here
*/ */
@ -296,32 +343,6 @@ class FtueAuthVariant(
?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) } ?.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<String>) {
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) { private fun handleRegistrationNavigation(flowResult: FlowResult) {
// Complete all mandatory stages first // Complete all mandatory stages first
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }

View file

@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor(
settings[forceDialPadDisplay] = force settings[forceDialPadDisplay] = force
} }
} }
private val forceLoginFallback = booleanPreferencesKey("force_login_fallback")
val forceLoginFallbackFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[forceLoginFallback].orFalse()
}
suspend fun setForceLoginFallbackFlow(force: Boolean) {
context.dataStore.edit { settings ->
settings[forceLoginFallback] = force
}
}
} }