Merge pull request #6587 from element-hq/feature/bma/flagsFromOnBoarding

Split developer settings into 2 screens to be able to access global settings when no logged in.
This commit is contained in:
Benoit Marty 2026-04-15 16:16:23 +02:00 committed by GitHub
commit 9699488ae5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1035 additions and 469 deletions

View file

@ -41,6 +41,7 @@ import io.element.android.features.login.impl.screens.createaccount.CreateAccoun
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
@ -67,6 +68,7 @@ class LoginFlowNode(
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val elementClassicConnection: ElementClassicConnection,
private val preferencesEntryPoint: PreferencesEntryPoint,
) : BaseFlowNode<LoginFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.CheckClassicFlow,
@ -117,6 +119,9 @@ class LoginFlowNode(
@Parcelize
data object QrCode : NavTarget
@Parcelize
data object AppDeveloperSettings : NavTarget
@Parcelize
data class ConfirmAccountProvider(
val isAccountCreation: Boolean,
@ -200,6 +205,10 @@ class LoginFlowNode(
backstack.push(NavTarget.CreateAccount(url))
}
override fun navigateToDeveloperSettings() {
backstack.push(NavTarget.AppDeveloperSettings)
}
override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword())
}
@ -220,6 +229,18 @@ class LoginFlowNode(
)
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
}
NavTarget.AppDeveloperSettings -> {
val callback = object : PreferencesEntryPoint.DeveloperSettingsCallback {
override fun onDone() {
backstack.pop()
}
}
preferencesEntryPoint.createAppDeveloperSettingsNode(
parentNode = this,
buildContext = buildContext,
callback = callback,
)
}
NavTarget.ChooseAccountProvider -> {
val callback = object : ChooseAccountProviderNode.Callback {
override fun navigateToOidc(oidcDetails: OidcDetails) {

View file

@ -42,6 +42,7 @@ class OnBoardingNode(
fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails)
fun navigateToCreateAccount(url: String)
fun navigateToDeveloperSettings()
fun onDone()
}
@ -75,6 +76,7 @@ class OnBoardingNode(
onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = callback::navigateToCreateAccount,
onBackClick = callback::onDone,
onDeveloperSettingsClick = callback::navigateToDeveloperSettings,
)
}
}

View file

@ -29,6 +29,7 @@ import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
import kotlinx.coroutines.launch
@ -125,6 +126,7 @@ class OnBoardingPresenter(
return OnBoardingState(
isAddingAccount = isAddingAccount,
showBackButton = params.showBackButton,
showDeveloperSettings = buildMeta.buildType != BuildType.RELEASE,
productionApplicationName = buildMeta.productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.architecture.AsyncData
data class OnBoardingState(
val isAddingAccount: Boolean,
val showBackButton: Boolean,
val showDeveloperSettings: Boolean,
val productionApplicationName: String,
val defaultAccountProvider: String?,
val mustChooseAccountProvider: Boolean,

View file

@ -31,6 +31,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
),
anOnBoardingState(
showBackButton = true,
showDeveloperSettings = true,
),
)
}
@ -38,6 +39,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
fun anOnBoardingState(
isAddingAccount: Boolean = false,
showBackButton: Boolean = false,
showDeveloperSettings: Boolean = false,
productionApplicationName: String = "Element",
defaultAccountProvider: String? = null,
mustChooseAccountProvider: Boolean = false,
@ -52,6 +54,7 @@ fun anOnBoardingState(
) = OnBoardingState(
isAddingAccount = isAddingAccount,
showBackButton = showBackButton,
showDeveloperSettings = showDeveloperSettings,
productionApplicationName = productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,

View file

@ -64,6 +64,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun OnBoardingView(
state: OnBoardingState,
onBackClick: () -> Unit,
onDeveloperSettingsClick: () -> Unit,
onSignInWithQrCode: () -> Unit,
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
onCreateAccount: () -> Unit,
@ -110,6 +111,7 @@ fun OnBoardingView(
loginView = loginView,
buttons = buttons,
onBackClick = onBackClick,
onDeveloperSettingsClick = onDeveloperSettingsClick,
)
}
}
@ -120,6 +122,7 @@ private fun AddFirstAccountScaffold(
loginView: @Composable () -> Unit,
buttons: @Composable () -> Unit,
onBackClick: () -> Unit,
onDeveloperSettingsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OnBoardingPage(
@ -136,6 +139,18 @@ private fun AddFirstAccountScaffold(
} else {
OnBoardingContent(state = state)
}
if (state.showDeveloperSettings) {
IconButton(
onClick = onDeveloperSettingsClick,
modifier = Modifier
.align(Alignment.TopStart),
) {
Icon(
imageVector = CompoundIcons.SettingsSolid(),
contentDescription = stringResource(CommonStrings.common_developer_options),
)
}
}
if (state.showBackButton) {
// Add icon button to "navigate back"
IconButton(
@ -334,6 +349,7 @@ internal fun OnBoardingViewPreview(
OnBoardingView(
state = state,
onBackClick = {},
onDeveloperSettingsClick = {},
onSignInWithQrCode = {},
onSignIn = {},
onCreateAccount = {},

View file

@ -16,6 +16,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
import io.element.android.features.preferences.test.FakePreferencesEntryPoint
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
@ -41,6 +42,7 @@ class DefaultLoginEntryPointTest {
oidcActionFlow = FakeOidcActionFlow(),
appCoroutineScope = backgroundScope,
elementClassicConnection = FakeElementClassicConnection(),
preferencesEntryPoint = FakePreferencesEntryPoint(),
)
}
val callback = object : LoginEntryPoint.Callback {

View file

@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.onboarding
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues
@ -46,11 +47,15 @@ class OnboardingViewTest {
rule.setOnboardingView(
state = anOnBoardingState(
canCreateAccount = true,
showDeveloperSettings = false,
eventSink = eventSink,
),
onCreateAccount = callback,
)
rule.clickOn(R.string.screen_onboarding_sign_up)
// Developer settings should not be shown
val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options)
rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist()
}
}
@ -172,6 +177,22 @@ class OnboardingViewTest {
}
}
@Test
fun `clicking on settings calls the developer settings callback`() {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setOnboardingView(
state = anOnBoardingState(
showDeveloperSettings = true,
eventSink = eventSink,
),
onDeveloperSettingsClick = callback,
)
val text = rule.activity.getString(CommonStrings.common_developer_options)
rule.onNodeWithContentDescription(text).performClick()
}
}
@Test
fun `cannot report a problem when the feature is disabled`() {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
@ -235,6 +256,7 @@ class OnboardingViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
state: OnBoardingState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(),
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onCreateAccount: () -> Unit = EnsureNeverCalled(),
@ -248,6 +270,7 @@ class OnboardingViewTest {
OnBoardingView(
state = state,
onBackClick = onBackClick,
onDeveloperSettingsClick = onDeveloperSettingsClick,
onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn,
onCreateAccount = onCreateAccount,