Skip to content

Conversation

@rafaeltonholo
Copy link
Member

@rafaeltonholo rafaeltonholo commented Nov 13, 2025

Resolves #10102.
Depends on #10131

This PR introduces the capability to enable and disable feature flags during runtime execution via the Secret debug screen.

Changes summary

  • Add new Secret Debug Section: Feature Flag
  • Add MutableFeatureFlagFactory (Available only on Debug builds)
  • Rename AccountSetupFinishedLauncher to MessageListLauncher

Screen Recording

Screen.Recording.2025-11-13.at.8.56.57.AM.mp4

@rafaeltonholo rafaeltonholo requested a review from a team as a code owner November 13, 2025 13:01
@rafaeltonholo rafaeltonholo added the merge block: soft freeze PR to main is blocked: risky code or feature flag enablement must wait until soft freeze lifts. label Nov 13, 2025
@rafaeltonholo rafaeltonholo force-pushed the feat/enable-feature-flag-switch-debug-builds branch from 4d7bd2d to 579ebc2 Compare November 13, 2025 13:22
@wmontwe wmontwe removed the merge block: soft freeze PR to main is blocked: risky code or feature flag enablement must wait until soft freeze lifts. label Nov 14, 2025
@rafaeltonholo rafaeltonholo force-pushed the feat/enable-feature-flag-switch-debug-builds branch from 579ebc2 to cbf6eb7 Compare November 14, 2025 12:28
Copy link
Member

@wmontwe wmontwe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the MutableFeatureFlagFactory mixes "define flags" and "mutate runtime overrides" in a way that feels hard to follow.

When the goal is that the factory defines the flags and their defaults while there is an optional runtime override, the best would be to keep "defaults" and "overrides" apart.

Rough idea:

interface FeatureFlagFactory {
    fun createCatalog(): List<FeatureFlag>
}

interface FeatureFlagOverrides {
    fun getAll(): Map<FeatureFlagKey, Boolean>
    fun get(key: FeatureFlagKey): Boolean?
    fun set(key: FeatureFlagKey, enabled: Boolean)
    fun clear(key: FeatureFlagKey)
    fun clearAll()
}

class InMemoryFeatureFlagProvider(
    private val factory: FeatureFlagFactory,
    private val overrides: FeatureFlagOverrides,
): FeatureFlagProvider {
    // cache defaults privately


    fun provide(key: FeatureFlagKey): FeatureFlagResult { 
        // answer from overrides first then fallback to defaults
    }
}

Then there could be a "in-memory" and a "no-op" version of FeatureFlagOverrides and injected based on the build type. This would allow us to use the override in daily too.

UI:

When changing a flag twice, the ui shows the modified indicator even when the setting returned to it's default state and requires to apply the changes. But there is no change to the defaults.

Design system additions are a good candidate for a separate PR.

@rafaeltonholo
Copy link
Member Author

Design system additions are a good candidate for a separate PR.

I have moved the implementation of the Horizontal Pager to the PR #10131. Also included the implementation for the HorizontalPagerPrimary

@rafaeltonholo rafaeltonholo force-pushed the feat/enable-feature-flag-switch-debug-builds branch from cbf6eb7 to fddbc53 Compare November 21, 2025 20:35
@rafaeltonholo
Copy link
Member Author

Hi @wmontwe, I've changed the implementation to something close to your suggestion. Please take a look and let me know if it is better now.

@rafaeltonholo rafaeltonholo force-pushed the feat/enable-feature-flag-switch-debug-builds branch 2 times, most recently from 501101f to 3810694 Compare November 25, 2025 14:05
Copy link
Member

@wmontwe wmontwe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments.

Non-blocking: this sparks an idea around reactive feature flags. Probably a separate task.

interface FeatureFlagFactory {
    fun createFeatureCatalog(): List<FeatureFlag>
}

// Change to:
interface FeatureCatalogProvider {
    fun getCatalog: Flow<List<FeatureFlag>>
}

then

class InMemoryFeatureFlagProvider(
    featureCatalogProvider: FeatureCatalogProvider,
    featureFlagOverrides: FeatureFlagOverrides,
    mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
    scope: CoroutineScope = CoroutineScope(SupervisorJob() + mainDispatcher),
) : FeatureFlagProvider {

    private val combinedFlags: StateFlow<Map<FeatureFlagKey, FeatureFlag>> =
        combine(
            featureCatalogProvider.getCatalog(),
            featureFlagOverrides.overrides,
        ) { defaultsList, overridesMap ->
            val defaults = defaultsList.associateBy { it.key }
            val overrides = overridesMap.mapValues {
                FeatureFlag(key = it.key, enabled = it.value)
            }
            defaults + overrides
        }.stateIn(
            scope = scope,
            started = SharingStarted.Eagerly,
            initialValue = emptyMap(),
        )

    override fun provide(key: FeatureFlagKey): FeatureFlagResult {
        val enabled: Boolean? = combinedFlags.value[key]?.enabled

        return when (enabled) {
            null -> FeatureFlagResult.Unavailable
            true -> FeatureFlagResult.Enabled
            false -> FeatureFlagResult.Disabled
        }
    }
}

@rafaeltonholo
Copy link
Member Author

@wmontwe, I've addressed all the comments and created a ticket (#10154) to migrate the FeatureFlagProvider to a reactive approach.

Copy link
Member

@wmontwe wmontwe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@rafaeltonholo rafaeltonholo merged commit 602d1e3 into thunderbird:main Nov 26, 2025
13 checks passed
@rafaeltonholo rafaeltonholo deleted the feat/enable-feature-flag-switch-debug-builds branch November 26, 2025 12:34
@thunderbird-botmobile thunderbird-botmobile bot added this to the Thunderbird 16 milestone Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce Feature Flag runtime change

2 participants