Skip to content

Conversation

@sztomek
Copy link
Contributor

@sztomek sztomek commented Jan 6, 2026

Description

This PR aims to cover an edge case we've identified last year.
Feature initialization happens in an async manner (when we're talking about prod builds) inside the ctor of FireabaseRemoteFeatureProvider. If a flag is queried while the remote config is being fetched, we simply return the default value.
That resulted the very awkward scenario with the onboarding feature launch -- people who just installed the app saw the old onboarding despite we had the flags properly configured on Firebase. The default value of Feature.NEW_ONBOARDING_ACCOUNT_CREATION was false at this time, we changed it later to true so the issue was seemingly solved.

This is my last attempt to fix the issue ultimately. My initial attemtps were:

  • use an androidx.startup.Initializer to init firebase provider. that was a dead end beacuse the initializer is not blocking the app launch flow
  • change logic on the MainActivity to init the feature providers in onCreate before doing anything else. that did not work fine either, i had many issues with how we launch the onboarding activity. multiple onboarding activities could be created or none of them were launched at all.

So i finally went with this approach and created a suspend function that will wait until all the providers are inited. Yeah, FeatureProvider.awaitInitialization has been also introduced for this purpose.

This isn't ideal either, because you'll need to 'know' that a feature flag is expected to be called early in the app launch flow...
The bulletproof path would be to rework our whole app launch flow and stay of splash until everything gets initialized.

Testing Instructions

  1. Uninstall the app
  2. Apply this patch onboarding-flag.patch that sets the default value to false and lets us use debug as prod.
  3. Launch the app and verify if you're seeing the new onboarding.

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

@sztomek sztomek added this to the 8.4 milestone Jan 6, 2026
Copilot AI review requested due to automatic review settings January 6, 2026 19:12
@sztomek sztomek requested a review from a team as a code owner January 6, 2026 19:12
@sztomek sztomek added the [Type] Enhancement Improve an existing feature. label Jan 6, 2026
@sztomek sztomek requested review from geekygecko and removed request for a team January 6, 2026 19:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses an edge case where the onboarding feature flag is queried before Firebase Remote Config finishes loading, causing new app installs to see the old onboarding flow despite remote configuration. The solution introduces an awaitInitialization() method to all feature providers and a new isEnabledWithRemote() function that suspends until remote config is fetched.

Key changes:

  • Added awaitInitialization() method to the FeatureProvider interface, implemented across all providers
  • Created isEnabledWithRemote() function that waits for remote config initialization with a configurable timeout
  • Updated onboarding flow to use the new async feature flag check to ensure remote values are loaded before rendering

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/featureflag/FeatureProvider.kt Adds awaitInitialization() method to the interface
modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/featureflag/providers/FirebaseRemoteFeatureProvider.kt Implements awaitInitialization() using CompletableDeferred that completes when Firebase fetch succeeds or fails
modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/featureflag/providers/PreferencesFeatureProvider.kt Implements awaitInitialization() returning true immediately (no async work needed)
modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/featureflag/providers/DefaultReleaseFeatureProvider.kt Implements awaitInitialization() returning true immediately (no async work needed)
modules/services/sharedtest/src/main/java/au/com/shiftyjelly/pocketcasts/sharedtest/InMemoryFeatureProvider.kt Implements awaitInitialization() returning true immediately for test provider
modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/featureflag/FeatureFlag.kt Adds isEnabledWithRemote() function that awaits all provider initialization with timeout before returning feature value
modules/features/account/src/main/java/au/com/shiftyjelly/pocketcasts/account/onboarding/OnboardingFlowComposable.kt Uses produceState with isEnabledWithRemote() to await remote config before building navigation graph
app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt Wraps notification permission check in coroutine to await remote feature flag value

Comment on lines +153 to +160
if (FeatureFlag.isEnabled(Feature.NEW_ONBOARDING_ACCOUNT_CREATION)) {
navController.popBackStack()
} else {
exitOnboarding(OnboardingExitInfo.Simple)
}
},
onComplete = {
val route = if (FeatureFlag.isEnabled(Feature.NEW_ONBOARDING_ACCOUNT_CREATION)) {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

After waiting for the remote config to load with isEnabledWithRemote(), these subsequent calls to isEnabled() for the same feature flag could potentially return inconsistent values. While the remote config should now be loaded, it's safer and clearer to use the already-loaded useNewOnboardingFlow value instead of making repeated synchronous checks that bypass the await logic.

Consider using the useNewOnboardingFlow variable (which comes from the awaited remote value) instead of calling FeatureFlag.isEnabled() again.

Suggested change
if (FeatureFlag.isEnabled(Feature.NEW_ONBOARDING_ACCOUNT_CREATION)) {
navController.popBackStack()
} else {
exitOnboarding(OnboardingExitInfo.Simple)
}
},
onComplete = {
val route = if (FeatureFlag.isEnabled(Feature.NEW_ONBOARDING_ACCOUNT_CREATION)) {
if (useNewOnboardingFlow) {
navController.popBackStack()
} else {
exitOnboarding(OnboardingExitInfo.Simple)
}
},
onComplete = {
val route = if (useNewOnboardingFlow) {

Copilot uses AI. Check for mistakes.
@geekygecko
Copy link
Member

@sztomek I would like to ask Michał to review this once he is back from AFK.

@sztomek sztomek requested a review from MiSikora January 12, 2026 19:39
Copy link
Contributor

@MiSikora MiSikora left a comment

Choose a reason for hiding this comment

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

What do you think about adjusting this slightly? Instead of exposing FeatureFlag.isEnabledWithRemote(), we could expose FeatureFlag.awaitRemoteProviders().

The main issue I see with isEnabledWithRemote() is that it does not compose well with the rest of the API surface, such as isEnabledFlow(), isEnabledForUser(), and isExclusiveToPatron(). Introducing an explicit awaitRemoteProviders() call would allow all feature-flag accessors to share the same initialization mechanism.

With this approach, usage would allow more flexibility.

produceState<Boolean?>(null) {
  FeatureFlag.awaitRemoteProviders()
  FeatureFlag.isEnabled(Feature.Foo) // Or any other getter
}

val enabledFlow = flow {
  FeatureFlag.awaitRemoteProviders()
  emitAll(FeatureFlag.isEnabledFlow(Feature.Foo))
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Feature flags [Type] Enhancement Improve an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants