From 98ec7d967a080d6cb8c8da984b5687d68bf3d872 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 May 2026 21:48:41 +0200 Subject: [PATCH 1/5] Add killswitch for the puppy guide feature --- .../help/center/HelpCenterPresenter.kt | 7 +++- .../help/center/HelpCenterViewModel.kt | 3 ++ .../help/center/di/HelpCenterModule.kt | 6 ++- .../center/home/HelpCenterHomeDestination.kt | 8 ++-- .../puppyguide/PuppyGuideDestination.kt | 10 +++++ .../center/puppyguide/PuppyGuideViewModel.kt | 38 +++++++++++++++---- .../flags/UnleashFeatureFlagProvider.kt | 2 + .../android/featureflags/flags/Feature.kt | 3 ++ 8 files changed, 65 insertions(+), 12 deletions(-) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt index 76d7ac8c5d..6bee76b99b 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt @@ -28,6 +28,8 @@ import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import kotlinx.coroutines.flow.collect @@ -102,6 +104,7 @@ internal class HelpCenterPresenter( private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val featureManager: FeatureManager, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: HelpCenterUiState): HelpCenterUiState { @@ -171,7 +174,8 @@ internal class HelpCenterPresenter( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, flow3 = getPuppyGuideUseCase.invoke(), - ) { quickLinks, faq, puppyGuideResult -> + flow4 = featureManager.isFeatureEnabled(Feature.PUPPY_GUIDE), + ) { quickLinks, faq, puppyGuideResult, puppyGuideEnabled -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -191,6 +195,7 @@ internal class HelpCenterPresenter( val questions = faq.getOrNull()?.commonFAQ ?: listOf() val puppyGuide = puppyGuideResult.getOrNull() val puppyGuidePresentation = when { + !puppyGuideEnabled -> null puppyGuide == null || puppyGuide.stories.isEmpty() -> null puppyGuide.isForYoungDog == true -> HelpCenterUiState.PuppyGuidePresentation.FullCard else -> HelpCenterUiState.PuppyGuidePresentation.QuickAction diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt index 5c325fb3c3..8cff04c63c 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt @@ -4,6 +4,7 @@ import com.hedvig.android.data.conversations.HasAnyActiveConversationUseCase import com.hedvig.android.feature.help.center.data.GetHelpCenterFAQUseCase import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase +import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.molecule.public.MoleculeViewModel internal class HelpCenterViewModel( @@ -11,6 +12,7 @@ internal class HelpCenterViewModel( hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, getPuppyGuideUseCase: GetPuppyGuideUseCase, + featureManager: FeatureManager, ) : MoleculeViewModel( initialState = HelpCenterUiState( topics = listOf(), @@ -26,5 +28,6 @@ internal class HelpCenterViewModel( hasAnyActiveConversationUseCase = hasAnyActiveConversationUseCase, getHelpCenterFAQUseCase = getHelpCenterFAQUseCase, getPuppyGuideUseCase = getPuppyGuideUseCase, + featureManager = featureManager, ), ) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt index f458898500..5fb5fc0d98 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt @@ -68,6 +68,7 @@ val helpCenterModule = module { hasAnyActiveConversationUseCase = get(), getHelpCenterFAQUseCase = get(), getPuppyGuideUseCase = get(), + featureManager = get(), ) } @@ -99,7 +100,10 @@ val helpCenterModule = module { } viewModel { - PuppyGuideViewModel(getPuppyGuideUseCase = get()) + PuppyGuideViewModel( + getPuppyGuideUseCase = get(), + featureManager = get(), + ) } viewModel { params -> diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt index 223654d697..95bcb53329 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt @@ -423,10 +423,12 @@ private fun ContentWithoutSearch( modifier = Modifier .padding(horizontal = 20.dp), ) { - HedvigText(stringResource(Res.string.HC_HOME_VIEW_QUESTION), + HedvigText( + stringResource(Res.string.HC_HOME_VIEW_QUESTION), modifier = Modifier.semantics { heading() - }) + }, + ) HedvigText( text = stringResource(Res.string.HC_HOME_VIEW_ANSWER), color = HedvigTheme.colorScheme.textSecondary, @@ -509,7 +511,7 @@ private fun PuppyGuideCard(onClick: () -> Unit, modifier: Modifier = Modifier) { .fillMaxWidth() .shadow(1.dp, HedvigTheme.shapes.cornerXLarge), ) { - Column{ + Column { Box(Modifier.align(Alignment.CenterHorizontally)) { Image( painter = painterResource(Res.drawable.hundar_badar_pet), diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt index 077c2385b5..efb862f663 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt @@ -85,6 +85,7 @@ import hedvig.resources.PUPPY_GUIDE_INFO import hedvig.resources.PUPPY_GUIDE_LABEL_READ import hedvig.resources.PUPPY_GUIDE_TITLE import hedvig.resources.Res +import hedvig.resources.general_back_button import hedvig.resources.hundar_badar_pet import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource @@ -129,6 +130,14 @@ private fun PuppyGuideScreen( ) } + PuppyGuideUiState.Disabled -> PuppyScaffold(navigateUp = onNavigateUp) { + HedvigErrorSection( + onButtonClick = onNavigateUp, + buttonText = stringResource(Res.string.general_back_button), + modifier = Modifier.weight(1f), + ) + } + PuppyGuideUiState.Loading -> HedvigFullScreenCenterAlignedProgress() is PuppyGuideUiState.Success -> PuppyGuideSuccessScreen( @@ -509,5 +518,6 @@ private class PuppyGuideUiStatePreviewProvider : ), PuppyGuideUiState.Loading, PuppyGuideUiState.Failure, + PuppyGuideUiState.Disabled, ), ) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt index 9be8702273..a9dc3eeb5a 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt @@ -2,6 +2,7 @@ package com.hedvig.android.feature.help.center.puppyguide import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -9,6 +10,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -16,19 +19,24 @@ import kotlinx.coroutines.flow.SharingStarted internal class PuppyGuideViewModel( getPuppyGuideUseCase: GetPuppyGuideUseCase, + featureManager: FeatureManager, ) : MoleculeViewModel( - presenter = PuppyGuidePresenter(getPuppyGuideUseCase), + presenter = PuppyGuidePresenter(getPuppyGuideUseCase, featureManager), initialState = PuppyGuideUiState.Loading, sharingStarted = SharingStarted.WhileSubscribed(), ) private class PuppyGuidePresenter( private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val featureManager: FeatureManager, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: PuppyGuideUiState): PuppyGuideUiState { var currentState by remember { mutableStateOf(lastState) } var loadIteration by remember { mutableIntStateOf(0) } + val puppyGuideEnabled by remember(featureManager) { + featureManager.isFeatureEnabled(Feature.PUPPY_GUIDE) + }.collectAsState(null) CollectEvents { event -> when (event) { @@ -36,12 +44,26 @@ private class PuppyGuidePresenter( } } - LaunchedEffect(loadIteration) { - getPuppyGuideUseCase.invoke().collect { response -> - currentState = response.fold( - ifLeft = { PuppyGuideUiState.Failure }, - ifRight = { puppyGuide -> PuppyGuideUiState.Success(puppyGuide.stories) }, - ) + LaunchedEffect(loadIteration, puppyGuideEnabled) { + when (puppyGuideEnabled) { + // Flag not resolved yet, keep showing the loading state. + null -> { + currentState = PuppyGuideUiState.Loading + } + + false -> { + currentState = PuppyGuideUiState.Disabled + } + + true -> { + currentState = PuppyGuideUiState.Loading + getPuppyGuideUseCase.invoke().collect { response -> + currentState = response.fold( + ifLeft = { PuppyGuideUiState.Failure }, + ifRight = { puppyGuide -> PuppyGuideUiState.Success(puppyGuide.stories) }, + ) + } + } } } @@ -59,4 +81,6 @@ internal sealed interface PuppyGuideUiState { data object Loading : PuppyGuideUiState data object Failure : PuppyGuideUiState + + data object Disabled : PuppyGuideUiState } diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index e0f93dafeb..f54bed6da5 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -36,6 +36,8 @@ internal class UnleashFeatureFlagProvider( Feature.DISABLE_REDEEM_CAMPAIGN -> hedvigUnleashClient.client.isEnabled("disable_redeem_campaign") Feature.ENABLE_CLAIM_HISTORY -> hedvigUnleashClient.client.isEnabled("enable_claim_history") + + Feature.PUPPY_GUIDE -> !hedvigUnleashClient.client.isEnabled("disable_puppy_guide") } }.distinctUntilChanged() } diff --git a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt index 2db61ba0e0..7a57c527d2 100644 --- a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -22,4 +22,7 @@ enum class Feature( ), DISABLE_REDEEM_CAMPAIGN("Disables the ability to redeem a campaign code"), ENABLE_CLAIM_HISTORY("Enables claim history"), + PUPPY_GUIDE( + "Controls whether the puppy guide is available in the help center. Backed by the disable_puppy_guide kill switch.", + ), } From a9bb741d1c77810a778e4a6b4621ec6f07c1fb01 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 5 Jun 2026 16:58:12 +0300 Subject: [PATCH 2/5] Initiate the process of cleaning up feature flags --- CLAUDE.md | 20 ++++ FEATURE_FLAG_CLEANUP_NOTES.md | 84 +++++++++++++++++ .../feature-flags/FEATURE_FLAG_DEFAULTS.md | 92 +++++++++++++++++++ .../featureflags/HedvigUnleashClient.kt | 7 +- .../featureflags/flags/FeatureUnleashKey.kt | 17 ++++ .../flags/UnleashFeatureFlagProvider.kt | 42 ++++----- 6 files changed, 236 insertions(+), 26 deletions(-) create mode 100644 FEATURE_FLAG_CLEANUP_NOTES.md create mode 100644 app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md create mode 100644 app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt diff --git a/CLAUDE.md b/CLAUDE.md index e7d6c72902..42cc9971ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -469,6 +469,26 @@ dependencies { 3. Build generates type-safe Kotlin code 4. Use generated query class in repository/use case +### Working with Feature Flags + +Feature flags are backed by Unleash. Before adding or changing a flag, read +`app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md` — it explains why we never use +the SDK's `defaultValue` parameter (Unleash Android SDK issue #141), how a flag's value is +resolved when Unleash has never been fetched, and when bootstrap is required. + +To add a new flag: +1. Add the enum value to `Feature` (commonMain) with a short explanation. +2. Map it to its raw Unleash key in `Feature.unleashKey` (androidMain). +3. Add it to the correct arm in `UnleashFeatureFlagProvider`: positive `isEnabled(key)` or + kill switch `!isEnabled(key)`. + +**IMPORTANT — always reconsider bootstrap when adding a feature:** Decide what the flag +should resolve to when it has *never been fetched* (offline first launch / fresh install +before the first poll returns). If the natural polarity default is acceptable, do nothing. +If a rollout needs the opposite default, add a `Toggle(...)` to the bootstrap list in +`HedvigUnleashClient.start(...)`. Never bootstrap an app-gating flag (e.g. +`UPDATE_NECESSARY`) into its blocking state — that can brick the app for offline users. + ### Working with Translations ```bash diff --git a/FEATURE_FLAG_CLEANUP_NOTES.md b/FEATURE_FLAG_CLEANUP_NOTES.md new file mode 100644 index 0000000000..b8ff5eff12 --- /dev/null +++ b/FEATURE_FLAG_CLEANUP_NOTES.md @@ -0,0 +1,84 @@ +# Feature flag cleanup — working notes + +Scratch doc to resume the feature-flag audit/cleanup later. Not meant to be committed +long-term. Last updated 2026-05-30. + +## Context + +We added the `PUPPY_GUIDE` kill switch (`disable_puppy_guide`) and, while doing so, +reworked how flag defaults/bootstrap work. The reasoning is documented permanently in +`app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md` and pointed to +from the repo `CLAUDE.md` ("Working with Feature Flags"). This notes file is about the +*next* step: retiring stale flags to reduce clutter. + +## Important caveat + +Code only reveals each flag's **age** and **read sites** — NOT the actual flip history or +current rollout %. That lives in the Unleash dashboard. Every "make permanent" +recommendation below is conditional on confirming in Unleash that the flag is at 100% in +production before removing it. + +Key distinction: +- **Rollout flags** (gradually turn a new feature on) are meant to die once at 100%. These + are the clutter. +- **Operational kill switches** (`UPDATE_NECESSARY`, `DISABLE_CHAT`) are meant to live + forever so we can react to incidents without a release. Age is irrelevant for these. + +## Audit of all 12 flags + +| Flag | Added | Read sites (excl. provider/tests) | Type | Recommendation | +|---|---|---|---|---| +| DISABLE_REDEEM_CAMPAIGN | 2025-03 | **none** | kill switch | **Delete — dead code** | +| MOVING_FLOW | 2022-05 | insurances, help-center | rollout | Make permanent (likely ON) | +| PAYMENT_SCREEN | 2023-02 | profile, insurances, help-center | rollout | Make permanent (likely ON) | +| EDIT_COINSURED | 2023-12 | insurances, help-center, reminders | rollout | Make permanent (likely ON) | +| HELP_CENTER | 2023-12 | home, profile | kill switch | Make permanent (now core — puppy guide lives in it) | +| TRAVEL_ADDON | 2024-12 | movingflow, addon-purchase, changetier, addons | rollout | Keep — still new, wide surface | +| ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES | 2025-03 | chat | rollout | Keep — recent | +| ENABLE_CLAIM_HISTORY | 2026-04 | home, profile, delete-account | rollout | Keep — brand new | +| PUPPY_GUIDE | 2026-05 | help-center | kill switch | Keep — just shipped | +| UPDATE_NECESSARY | 2022-05 | HedvigAppState | operational kill switch | **Keep forever** — app-version gate | +| DISABLE_CHAT | 2023-09 | home | operational kill switch | **Keep forever** — disable chat during incidents | +| TERMINATION_FLOW | 2023-02 | insurances, data-termination | kill switch | Keep / confirm — may be deliberate legal/ops switch | + +## Action plan + +### 1. Safe, unambiguous win — delete `DISABLE_REDEEM_CAMPAIGN` (dead code) +Defined in the enum, `unleashKey`, and the provider, but read **nowhere** in the app +(verified by grep — no references outside those three files). Safe to remove regardless of +dashboard state. +- Remove the enum value from `Feature.kt` (commonMain). +- Remove its arm from `Feature.unleashKey` (`FeatureUnleashKey.kt`, androidMain). +- Remove its arm from `UnleashFeatureFlagProvider` (it's in the positive `isEnabled` group). + +### 2. Make permanent — after confirming 100% rollout in Unleash +`MOVING_FLOW`, `PAYMENT_SCREEN`, `EDIT_COINSURED`, `HELP_CENTER`. For each: +- Delete the enum value, `unleashKey` arm, and provider arm. +- At each read site, delete the flag branch and collapse to the enabled path (remove the + `combine` / `flatMapLatest` arm that gated on the flag). +- Delete the related test cases / `FakeFeatureManager` entries. + +Read sites to touch (from the audit grep): +- MOVING_FLOW: `GetInsuranceContractsUseCase`, `GetMemberActionsUseCase`. +- PAYMENT_SCREEN: `ProfileViewModel`, `GetMemberActionsUseCase`, (+ insurances test data). +- EDIT_COINSURED: `GetInsuranceContractsUseCase`, `GetInsuranceForEditCoInsuredUseCase`, + `GetMemberActionsUseCase`, `GetNeedsCoInsuredInfoRemindersUseCase`. +- HELP_CENTER: `GetHomeDataUseCase`, `ProfileViewModel` (+ profile/home tests). + +### 3. Keep — no action +Operational kill switches: `UPDATE_NECESSARY`, `DISABLE_CHAT`. +Too new / still rolling out: `TRAVEL_ADDON`, `ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES`, +`ENABLE_CLAIM_HISTORY`, `PUPPY_GUIDE`. + +### 4. Confirm intent — `TERMINATION_FLOW` +Old enough to retire, but `disable_termination_flow` may be a deliberate legal/operational +lever. Check before treating as stale. + +## Where things live (for when resuming) +- Enum: `app/featureflags/feature-flags/src/commonMain/.../flags/Feature.kt` +- Key map: `app/featureflags/feature-flags/src/androidMain/.../flags/FeatureUnleashKey.kt` +- Resolution + negation: `app/featureflags/feature-flags/src/androidMain/.../flags/UnleashFeatureFlagProvider.kt` +- Bootstrap: `app/featureflags/feature-flags/src/androidMain/.../HedvigUnleashClient.kt` +- Verify a build with: `./gradlew :feature-flags:ktlintFormat :feature-flags:compileAndroidMain` +- iOS equivalent flags live in the separate repo at `../ugglan` (different flag names; e.g. + `help_center` positive there vs `disable_help_center` here). diff --git a/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md b/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md new file mode 100644 index 0000000000..8a1dcf06c6 --- /dev/null +++ b/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md @@ -0,0 +1,92 @@ +# Feature flag defaults & the Unleash "never fetched" problem + +This doc explains how feature-flag *defaults* work in the app, why we don't use the +SDK's `defaultValue` parameter, and how to reason about a flag's value when Unleash +has never been fetched (offline first launch, fresh install before the first poll +returns, etc.). Read this before adding a new flag. + +## TL;DR + +- We **only** call `client.isEnabled(name)`. We **never** call the + `isEnabled(name, defaultValue)` overload — it's broken with the Frontend API. +- An absent toggle reads as `false`. We control the real default through two levers: + 1. **Flag naming polarity** (`enable_x` vs `disable_x`) + explicit negation at the + read site in `UnleashFeatureFlagProvider`. + 2. **Bootstrap** — only for the one flag where polarity alone gives the wrong default. +- Only `PUPPY_GUIDE` is bootstrapped today. Adding others is usually noise and, for + app-gating flags like `UPDATE_NECESSARY`, actively dangerous. + +## The bug: Unleash Android SDK issue #141 + +The Frontend API (`/api/frontend`) **only returns toggles that are enabled**. Disabled +or unknown toggles simply aren't in the response. The SDK's +`isEnabled(name, defaultValue = true)` overload is supposed to fall back to +`defaultValue` when a toggle is missing, but it doesn't — it returns `false` regardless. +See https://github.com/Unleash/unleash-android-sdk/issues/141. + +The important takeaway: **the bug lives entirely in that one deprecated overload.** The +plain `isEnabled(name)` call is well-defined — it returns `false` for any toggle the SDK +hasn't seen. As long as we never pass a `defaultValue`, we're not exposed to #141. + +## How a flag resolves to a value + +`isEnabled(name)` returns `false` for an absent toggle. We turn that into a +feature-enabled boolean in `UnleashFeatureFlagProvider`, choosing the polarity per flag: + +- **Positive flags** (`enable_x`, `payment_screen`, `moving_flow`, `update_necessary`…) + read `isEnabled(key)` directly. Absent → `false` → feature **off**. Good default for + new features: they stay off until we explicitly turn them on remotely. + +- **Kill switches** (`disable_x`) read `!isEnabled(key)`. Absent → `true` → feature + **on**. The feature is normally available, and the remote toggle is a switch we flip to + turn it *off*. When offline we can't fetch the switch, so the feature stays on — that's + an inherent and acceptable property of a kill switch. + +## When the "never fetched" default actually matters + +Thanks to `LocalBackup`, the SDK persists the last successfully-fetched toggle state and +reloads it on subsequent launches. So the never-fetched default only bites in a narrow +window: + +- The very first launch, before the first poll returns, **and** +- Fresh install while fully offline. + +After any successful fetch, an offline launch uses the last-known remote state, not the +bootstrap/absent default. + +## Bootstrap: when and why + +`HedvigUnleashClient.start(bootstrap = …)` seeds toggle state before the first fetch. +Bootstrap is only needed when the **desired** never-fetched default differs from the +**natural** polarity default. + +Today the only entry is: + +```kotlin +client.start(bootstrap = listOf(Toggle(name = Feature.PUPPY_GUIDE.unleashKey, enabled = true))) +``` + +`disable_puppy_guide` is a kill switch, so its natural absent default is "feature on". +But during rollout we want the puppy guide **hidden** until the first fetch confirms it +should show. Polarity gives the wrong default, so we bootstrap `enabled = true` (kill +switch on → feature hidden). Once toggles are fetched, the remote value takes over — +the bootstrap is discarded wholesale on the first successful fetch. + +### Do NOT bootstrap app-gating flags to the "blocking" state + +`UPDATE_NECESSARY` is the cautionary example. `update_necessary` is positive, so absent → +`false` → the app does **not** force an update → offline users can still use the app. +That's the safe direction. Bootstrapping it to `true` would brick the app for anyone who +is offline on first launch. Leave it alone. + +## Adding a new flag — checklist + +1. Add the enum value to `Feature` (commonMain) with a short explanation. +2. Add its raw Unleash key to `Feature.unleashKey` (androidMain). +3. Add it to the correct arm in `UnleashFeatureFlagProvider`: + - positive `isEnabled(key)`, or + - kill switch `!isEnabled(key)`. +4. Ask: **what should this be when never fetched / offline on first launch?** + - If the natural polarity default is acceptable → done, no bootstrap. + - If you need the opposite default during rollout → add a `Toggle(...)` to the + bootstrap list. Double-check you're not gating the whole app into a blocked state. diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt index 8eb66916be..39138162f0 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt @@ -2,9 +2,12 @@ package com.hedvig.android.featureflags import android.content.Context import com.hedvig.android.auth.MemberIdService +import com.hedvig.android.featureflags.flags.Feature +import com.hedvig.android.featureflags.flags.unleashKey import com.hedvig.android.logger.logcat import io.getunleash.android.DefaultUnleash import io.getunleash.android.UnleashConfig +import io.getunleash.android.data.Toggle import io.getunleash.android.data.UnleashContext import io.getunleash.android.events.HeartbeatEvent import io.getunleash.android.events.UnleashFetcherHeartbeatListener @@ -66,7 +69,9 @@ class HedvigUnleashClient( ) } } - client.start() + // Bootstrap the puppy guide kill switch to on, so the feature stays hidden until the first + // successful fetch. Once toggles are fetched, the remote value takes over. + client.start(bootstrap = listOf(Toggle(name = Feature.PUPPY_GUIDE.unleashKey, enabled = true))) } private fun createConfig(): UnleashConfig { diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt new file mode 100644 index 0000000000..23c40c4d75 --- /dev/null +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt @@ -0,0 +1,17 @@ +package com.hedvig.android.featureflags.flags + +internal val Feature.unleashKey: String + get() = when (this) { + Feature.DISABLE_CHAT -> "disable_chat" + Feature.MOVING_FLOW -> "moving_flow" + Feature.PAYMENT_SCREEN -> "payment_screen" + Feature.TERMINATION_FLOW -> "disable_termination_flow" + Feature.UPDATE_NECESSARY -> "update_necessary" + Feature.EDIT_COINSURED -> "edit_coinsured" + Feature.HELP_CENTER -> "disable_help_center" + Feature.TRAVEL_ADDON -> "enable_addons" + Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES -> "enable_video_player_in_chat_messages" + Feature.DISABLE_REDEEM_CAMPAIGN -> "disable_redeem_campaign" + Feature.ENABLE_CLAIM_HISTORY -> "enable_claim_history" + Feature.PUPPY_GUIDE -> "disable_puppy_guide" + } diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index f54bed6da5..4efa37e63f 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -12,32 +12,24 @@ internal class UnleashFeatureFlagProvider( override fun isFeatureEnabled(feature: Feature): Flow { return hedvigUnleashClient.featureUpdatedFlow .map { + val key = feature.unleashKey when (feature) { - Feature.DISABLE_CHAT -> hedvigUnleashClient.client.isEnabled("disable_chat") - - Feature.MOVING_FLOW -> hedvigUnleashClient.client.isEnabled("moving_flow") - - Feature.PAYMENT_SCREEN -> hedvigUnleashClient.client.isEnabled("payment_screen") - - Feature.TERMINATION_FLOW -> !hedvigUnleashClient.client.isEnabled("disable_termination_flow") - - Feature.UPDATE_NECESSARY -> hedvigUnleashClient.client.isEnabled("update_necessary") - - Feature.EDIT_COINSURED -> hedvigUnleashClient.client.isEnabled("edit_coinsured") - - Feature.HELP_CENTER -> !hedvigUnleashClient.client.isEnabled("disable_help_center") - - Feature.TRAVEL_ADDON -> hedvigUnleashClient.client.isEnabled("enable_addons") - - Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES -> hedvigUnleashClient.client.isEnabled( - "enable_video_player_in_chat_messages", - ) - - Feature.DISABLE_REDEEM_CAMPAIGN -> hedvigUnleashClient.client.isEnabled("disable_redeem_campaign") - - Feature.ENABLE_CLAIM_HISTORY -> hedvigUnleashClient.client.isEnabled("enable_claim_history") - - Feature.PUPPY_GUIDE -> !hedvigUnleashClient.client.isEnabled("disable_puppy_guide") + // Kill switches: the remote toggle being on means the feature is off. + Feature.TERMINATION_FLOW, + Feature.HELP_CENTER, + Feature.PUPPY_GUIDE, + -> !hedvigUnleashClient.client.isEnabled(key) + + Feature.DISABLE_CHAT, + Feature.MOVING_FLOW, + Feature.PAYMENT_SCREEN, + Feature.UPDATE_NECESSARY, + Feature.EDIT_COINSURED, + Feature.TRAVEL_ADDON, + Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES, + Feature.DISABLE_REDEEM_CAMPAIGN, + Feature.ENABLE_CLAIM_HISTORY, + -> hedvigUnleashClient.client.isEnabled(key) } }.distinctUntilChanged() } From 86f10c1b00a2c41c90006a7f4d3626459795a6e5 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Sat, 6 Jun 2026 01:05:29 +0300 Subject: [PATCH 3/5] Retire matured feature flags and make their features permanent Remove MOVING_FLOW, PAYMENT_SCREEN, EDIT_COINSURED and HELP_CENTER (all at 100% rollout) along with the dead DISABLE_REDEEM_CAMPAIGN, collapsing each flag check to its enabled behavior. Also wire ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to its Unleash key. --- FEATURE_FLAG_CLEANUP_NOTES.md | 10 +- .../GetInsuranceForEditCoInsuredUseCase.kt | 64 ++++----- .../center/data/GetMemberActionsUseCase.kt | 51 +++----- .../kotlin/GetMemberActionsUseCaseImpl.kt | 18 --- .../home/home/data/GetHomeDataUseCase.kt | 12 +- .../home/home/data/GetHomeUseCaseTest.kt | 44 ------- .../data/GetInsuranceContractsUseCase.kt | 55 ++++---- .../GetInsuranceContractsUseCaseImplTest.kt | 6 - .../feature/profile/tab/ProfileViewModel.kt | 4 +- .../profile/tab/ProfilePresenterTest.kt | 123 +----------------- .../featureflags/flags/FeatureUnleashKey.kt | 7 +- .../flags/UnleashFeatureFlagProvider.kt | 5 - .../android/featureflags/flags/Feature.kt | 6 - .../GetNeedsCoInsuredInfoRemindersUseCase.kt | 44 ++----- 14 files changed, 97 insertions(+), 352 deletions(-) diff --git a/FEATURE_FLAG_CLEANUP_NOTES.md b/FEATURE_FLAG_CLEANUP_NOTES.md index b8ff5eff12..b13ad2e696 100644 --- a/FEATURE_FLAG_CLEANUP_NOTES.md +++ b/FEATURE_FLAG_CLEANUP_NOTES.md @@ -43,13 +43,9 @@ Key distinction: ## Action plan -### 1. Safe, unambiguous win — delete `DISABLE_REDEEM_CAMPAIGN` (dead code) -Defined in the enum, `unleashKey`, and the provider, but read **nowhere** in the app -(verified by grep — no references outside those three files). Safe to remove regardless of -dashboard state. -- Remove the enum value from `Feature.kt` (commonMain). -- Remove its arm from `Feature.unleashKey` (`FeatureUnleashKey.kt`, androidMain). -- Remove its arm from `UnleashFeatureFlagProvider` (it's in the positive `isEnabled` group). +### 1. Safe, unambiguous win — delete `DISABLE_REDEEM_CAMPAIGN` (dead code) — DONE 2026-05-30 +Was defined in the enum, `unleashKey`, and the provider, but read **nowhere** in the app. +Removed from all three files. ### 2. Make permanent — after confirming 100% rollout in Unleash `MOVING_FLOW`, `PAYMENT_SCREEN`, `EDIT_COINSURED`, `HELP_CENTER`. For each: diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt index 2ded7e3009..0fa5d2016c 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt @@ -7,14 +7,11 @@ import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn -import kotlinx.coroutines.flow.first import octopus.AvailableSelfServiceOnContractsQuery internal interface GetInsuranceForEditCoInsuredUseCase { @@ -26,9 +23,7 @@ internal interface GetInsuranceForEditCoInsuredUseCase { @Inject internal class GetInsuranceForEditCoInsuredUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, -) : - GetInsuranceForEditCoInsuredUseCase { +) : GetInsuranceForEditCoInsuredUseCase { override suspend fun invoke(): Either> { return either { val contracts = apolloClient.query(AvailableSelfServiceOnContractsQuery()) @@ -38,38 +33,35 @@ internal class GetInsuranceForEditCoInsuredUseCaseImpl( .currentMember .activeContracts buildList { - val isEditCoInsuredEnabled = featureManager.isFeatureEnabled(Feature.EDIT_COINSURED).first() - if (isEditCoInsuredEnabled) { - contracts.filter { it.supportsCoInsured }.forEach { contract -> - val destination = if (contract.coInsured?.any { it.hasMissingInfo } == true) { - QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo(contract.id) - } else { - QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove(contract.id) - } - add( - InsuranceForEditOrAddCoInsured( - quickLinkDestination = destination, - displayName = contract.currentAgreement.productVariant.displayName, - exposureName = contract.exposureDisplayName, - id = contract.id, - ), - ) + contracts.filter { it.supportsCoInsured }.forEach { contract -> + val destination = if (contract.coInsured?.any { it.hasMissingInfo } == true) { + QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo(contract.id) + } else { + QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove(contract.id) } - contracts.filter { it.supportsCoOwners }.forEach { contract -> - val destination = if (contract.coOwners?.any { it.hasMissingInfo } == true) { - QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo(contract.id) - } else { - QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove(contract.id) - } - add( - InsuranceForEditOrAddCoInsured( - quickLinkDestination = destination, - displayName = contract.currentAgreement.productVariant.displayName, - exposureName = contract.exposureDisplayName, - id = contract.id, - ), - ) + add( + InsuranceForEditOrAddCoInsured( + quickLinkDestination = destination, + displayName = contract.currentAgreement.productVariant.displayName, + exposureName = contract.exposureDisplayName, + id = contract.id, + ), + ) + } + contracts.filter { it.supportsCoOwners }.forEach { contract -> + val destination = if (contract.coOwners?.any { it.hasMissingInfo } == true) { + QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo(contract.id) + } else { + QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove(contract.id) } + add( + InsuranceForEditOrAddCoInsured( + quickLinkDestination = destination, + displayName = contract.currentAgreement.productVariant.displayName, + exposureName = contract.exposureDisplayName, + id = contract.id, + ), + ) } } } diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt index 27b50102d2..1e64e37333 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt @@ -2,21 +2,17 @@ package com.hedvig.android.feature.help.center.data import arrow.core.Either import arrow.core.raise.either -import arrow.fx.coroutines.parZip import com.apollographql.apollo.ApolloClient import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat import com.hedvig.android.shared.partners.deflect.DeflectData import com.hedvig.android.ui.emergency.FirstVetSection import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn -import kotlinx.coroutines.flow.first import octopus.MemberActionsQuery @ContributesBinding(AppScope::class) @@ -24,40 +20,25 @@ import octopus.MemberActionsQuery @Inject internal class GetMemberActionsUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetMemberActionsUseCase { override suspend fun invoke(): Either { return either { - parZip( - { featureManager.isFeatureEnabled(Feature.EDIT_COINSURED).first() }, - { featureManager.isFeatureEnabled(Feature.MOVING_FLOW).first() }, - { featureManager.isFeatureEnabled(Feature.PAYMENT_SCREEN).first() }, - { - apolloClient - .query(MemberActionsQuery()) - .safeExecute(::ErrorMessage) - .onLeft { logcat { "Cannot load memberActions: $it" } } - .bind().currentMember.memberActions - }, - ) { - isCoInsuredFeatureOn, - isMovingFeatureOn, - isConnectPaymentFeatureOn, - memberActions, - -> - MemberAction( - isCancelInsuranceEnabled = memberActions?.isCancelInsuranceEnabled ?: false, - isConnectPaymentEnabled = - isConnectPaymentFeatureOn && memberActions?.isConnectPaymentEnabled ?: false, - isEditCoInsuredEnabled = isCoInsuredFeatureOn && memberActions?.isEditCoInsuredEnabled ?: false, - isEditCoOwnersEnabled = isCoInsuredFeatureOn && memberActions?.isEditCoOwnersEnabled ?: false, - isMovingEnabled = isMovingFeatureOn && memberActions?.isMovingEnabled ?: false, - isTravelCertificateEnabled = memberActions?.isTravelCertificateEnabled ?: false, - sickAbroadAction = memberActions?.sickAbroadDeflect.toSickAbroadAction(), - firstVetAction = memberActions?.firstVetAction?.toVetAction(), - isTierChangeEnabled = memberActions?.isChangeTierEnabled ?: false, - ) - } + val memberActions = apolloClient + .query(MemberActionsQuery()) + .safeExecute(::ErrorMessage) + .onLeft { logcat { "Cannot load memberActions: $it" } } + .bind().currentMember.memberActions + MemberAction( + isCancelInsuranceEnabled = memberActions?.isCancelInsuranceEnabled ?: false, + isConnectPaymentEnabled = memberActions?.isConnectPaymentEnabled ?: false, + isEditCoInsuredEnabled = memberActions?.isEditCoInsuredEnabled ?: false, + isEditCoOwnersEnabled = memberActions?.isEditCoOwnersEnabled ?: false, + isMovingEnabled = memberActions?.isMovingEnabled ?: false, + isTravelCertificateEnabled = memberActions?.isTravelCertificateEnabled ?: false, + sickAbroadAction = memberActions?.sickAbroadDeflect.toSickAbroadAction(), + firstVetAction = memberActions?.firstVetAction?.toVetAction(), + isTierChangeEnabled = memberActions?.isChangeTierEnabled ?: false, + ) } } } diff --git a/app/feature/feature-help-center/src/test/kotlin/GetMemberActionsUseCaseImpl.kt b/app/feature/feature-help-center/src/test/kotlin/GetMemberActionsUseCaseImpl.kt index 7389c2e71e..ae158b2bac 100644 --- a/app/feature/feature-help-center/src/test/kotlin/GetMemberActionsUseCaseImpl.kt +++ b/app/feature/feature-help-center/src/test/kotlin/GetMemberActionsUseCaseImpl.kt @@ -10,8 +10,6 @@ import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.test.isRight import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCaseImpl import com.hedvig.android.feature.help.center.data.MemberAction -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.test.runTest import octopus.MemberActionsQuery @@ -75,16 +73,8 @@ class GetMemberActionsUseCaseImplTest { @Test fun `when response has isChangeTierEnabled as true MemberAction should have isTierChangeEnabled as true`() = runTest { - val featureManager = FakeFeatureManager( - fixedMap = mapOf( - Feature.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, - ), - ) val subjectUseCase = GetMemberActionsUseCaseImpl( apolloClient = apolloClientWithGoodResponseTierChangeTrue, - featureManager = featureManager, ) val result = subjectUseCase.invoke() assertk.assertThat(result) @@ -96,16 +86,8 @@ class GetMemberActionsUseCaseImplTest { @Test fun `when response has isChangeTierEnabled as false MemberAction should have isTierChangeEnabled as false`() = runTest { - val featureManager = FakeFeatureManager( - fixedMap = mapOf( - Feature.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, - ), - ) val subjectUseCase = GetMemberActionsUseCaseImpl( apolloClient = apolloClientWithGoodResponseTierChangeFalse, - featureManager = featureManager, ) val result = subjectUseCase.invoke() assertk.assertThat(result) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index 6c4fe1dbda..6c96581a10 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -83,7 +83,6 @@ internal class GetHomeDataUseCaseImpl( flow { emitAll(getTravelAddonBannerInfoUseCaseProvider.provide().invoke(AddonBannerSource.INSURANCES_TAB)) }, - featureManager.isFeatureEnabled(Feature.HELP_CENTER), featureManager.isFeatureEnabled(Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT), hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), ) { @@ -91,7 +90,6 @@ internal class GetHomeDataUseCaseImpl( unreadMessageCountResult, memberReminders, travelBannerInfo, - isHelpCenterEnabled, inboxAlwaysAvailable, anyActiveConversations, -> @@ -173,7 +171,7 @@ internal class GetHomeDataUseCaseImpl( veryImportantMessages = veryImportantMessages, memberReminders = memberReminders, hasUnseenChatMessages = hasUnseenChatMessages, - showHelpCenter = isHelpCenterEnabled, + showHelpCenter = true, firstVetSections = firstVetActions, crossSells = crossSells, travelBannerInfo = travelBannerInfo?.firstOrNull(), @@ -325,16 +323,15 @@ data class HomeData( /** * The reason this exists is because the standard combine function only allows up to 5 generic flows. */ -fun combine( +fun combine( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, flow6: Flow, - flow7: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> -> + transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> @Suppress("UNCHECKED_CAST") transform( args[0] as T1, @@ -343,6 +340,5 @@ fun combine( args[3] as T4, args[4] as T5, args[5] as T6, - args[6] as T7, ) } diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt index f34bfbd4ef..14443a8217 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt @@ -478,7 +478,6 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to true, // With the inbox-always-available kill switch off, the icon depends purely on existing conversations Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to false, @@ -543,7 +542,6 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to true, Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to inboxAlwaysAvailable, ), @@ -597,47 +595,6 @@ internal class GetHomeUseCaseTest { } } - @Test - fun `the disable help center feature flag determines if we show it or not`( - @TestParameter helpCenterIsEnabled: Boolean, - ) = runTest { - val featureManager = FakeFeatureManager( - mapOf( - Feature.HELP_CENTER to helpCenterIsEnabled, - Feature.ENABLE_CLAIM_HISTORY to true, - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to false, - ), - ) - val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) - - apolloClient.registerTestResponse( - HomeQuery(true), - HomeQuery.Data(OctopusFakeResolver), - ) - apolloClient.registerTestResponse( - UnreadMessageCountQuery(), - UnreadMessageCountQuery.Data(OctopusFakeResolver), - ) - apolloClient.registerTestResponse( - CbmNumberOfChatMessagesQuery(), - CbmNumberOfChatMessagesQuery.Data(OctopusFakeResolver), - ) - - val result = getHomeDataUseCase.invoke(true).first() - - assertThat(result) - .isNotNull() - .isRight() - .prop(HomeData::showHelpCenter) - .apply { - if (helpCenterIsEnabled) { - isTrue() - } else { - isFalse() - } - } - } - @Test fun `without legacy conversations, show the chat icon depending on the other conversations status`( @TestParameter hasAtLeastOneOpenConversation: Boolean, @@ -645,7 +602,6 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to true, // Inbox-always-available off, so the icon reflects the conversation state being tested here Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to false, diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt index 60df0cef75..deb989aa69 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt @@ -29,10 +29,10 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive import octopus.InsuranceContractsQuery import octopus.fragment.AgreementDisplayItemFragment @@ -54,46 +54,41 @@ internal class GetInsuranceContractsUseCaseImpl( private val featureManager: FeatureManager, ) : GetInsuranceContractsUseCase { override fun invoke(): Flow>> { - return combine( - featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).flatMapLatest { areAddonsEnabled -> - flow { - while (currentCoroutineContext().isActive) { - emitAll( - apolloClient - .query( - InsuranceContractsQuery( - addonsEnabled = areAddonsEnabled, - options = Optional.present( - DisplayItemOptions( - hidePrice = Optional.present(true), - hideAddons = Optional.present(true), - ), + return featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).flatMapLatest { areAddonsEnabled -> + flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient + .query( + InsuranceContractsQuery( + addonsEnabled = areAddonsEnabled, + options = Optional.present( + DisplayItemOptions( + hidePrice = Optional.present(true), + hideAddons = Optional.present(true), ), ), - ) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .safeFlow(::ErrorMessage), - ) - delay(3.seconds) - } + ), + ) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage), + ) + delay(3.seconds) } - }, - featureManager.isFeatureEnabled(Feature.EDIT_COINSURED), - featureManager.isFeatureEnabled(Feature.MOVING_FLOW), - ) { insuranceQueryResponse, isEditCoInsuredEnabled, isMovingFlowFlagEnabled -> + } + }.map { insuranceQueryResponse -> either { val insuranceQueryData = insuranceQueryResponse.bind() val contractHolderDisplayName = insuranceQueryData.getContractHolderDisplayName() val contractHolderSSN = insuranceQueryData.currentMember.ssn?.let { formatSsn(it) } val isMovingEnabledForMember = - insuranceQueryData.currentMember.memberActions?.isMovingEnabled == true && isMovingFlowFlagEnabled + insuranceQueryData.currentMember.memberActions?.isMovingEnabled == true val terminatedContracts = insuranceQueryData.currentMember.terminatedContracts.map { it.toContract( isTerminated = true, contractHolderDisplayName = contractHolderDisplayName, contractHolderSSN = contractHolderSSN, - isEditCoInsuredEnabled = isEditCoInsuredEnabled, isMovingFlowEnabled = isMovingEnabledForMember, ) } @@ -103,7 +98,6 @@ internal class GetInsuranceContractsUseCaseImpl( isTerminated = false, contractHolderDisplayName = contractHolderDisplayName, contractHolderSSN = contractHolderSSN, - isEditCoInsuredEnabled = isEditCoInsuredEnabled, isMovingFlowEnabled = isMovingEnabledForMember, ) } @@ -155,7 +149,6 @@ private fun ContractFragment.toContract( isTerminated: Boolean, contractHolderDisplayName: String, contractHolderSSN: String?, - isEditCoInsuredEnabled: Boolean, isMovingFlowEnabled: Boolean, ): EstablishedInsuranceContract { return EstablishedInsuranceContract( @@ -209,8 +202,8 @@ private fun ContractFragment.toContract( ) }, supportsAddressChange = supportsMoving && isMovingFlowEnabled, - supportsEditCoInsured = supportsCoInsured && isEditCoInsuredEnabled, - supportsEditCoOwners = supportsCoOwners && isEditCoInsuredEnabled, + supportsEditCoInsured = supportsCoInsured, + supportsEditCoOwners = supportsCoOwners, isTerminated = isTerminated, supportsTierChange = supportsChangeTier, existingAddons = existingAddons?.map { diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt index d8c1160197..63ddc6094b 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt @@ -125,9 +125,6 @@ class GetInsuranceContractsUseCaseImplTest { runTest { val featureManager = FakeFeatureManager( fixedMap = mapOf( - Feature.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, Feature.TRAVEL_ADDON to false, ), ) @@ -150,9 +147,6 @@ class GetInsuranceContractsUseCaseImplTest { runTest { val featureManager = FakeFeatureManager( fixedMap = mapOf( - Feature.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, Feature.TRAVEL_ADDON to false, ), ) diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt index f13c5f371a..2adb51064d 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt @@ -80,20 +80,18 @@ internal class ProfilePresenter( } combine( getMemberRemindersUseCase.invoke(), - featureManager.isFeatureEnabled(Feature.PAYMENT_SCREEN), featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_HISTORY), flow { emit(getEuroBonusStatusUseCase.invoke()) }, flow { emit(checkCertificatesAvailabilityUseCase.invoke()) }, ) { memberReminders, - isPaymentScreenFeatureEnabled, isClaimHistoryFeatureEnabled, eurobonusResponse, certificatesAvailability, -> ProfileUiState.Success( euroBonus = eurobonusResponse.getOrNull(), - showPaymentScreen = isPaymentScreenFeatureEnabled, + showPaymentScreen = true, memberReminders = memberReminders, showClaimHistory = isClaimHistoryFeatureEnabled, certificatesAvailable = certificatesAvailability.isRight(), diff --git a/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt b/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt index fceebd00f7..4ff70f5e0e 100644 --- a/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt +++ b/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt @@ -42,108 +42,6 @@ class ProfilePresenterTest { } } - @Test - fun `when payment-feature is not activated, should not show payment-data`() = runTest { - val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() - val presenter = ProfilePresenter( - FakeGetEurobonusStatusUseCase().apply { - turbine.add(GetEurobonusError.EurobonusNotApplicable.left()) - }, - certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, - TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, - TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - fixedMap = mapOf( - Feature.PAYMENT_SCREEN to false, - Feature.HELP_CENTER to true, - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), - noopLogoutUseCase, - ) - - presenter.test(ProfileUiState.Loading) { - assertThat(awaitItem()).isInstanceOf() - runCurrent() - assertThat(awaitItem()).isEqualTo( - ProfileUiState.Success( - euroBonus = null, - certificatesAvailable = true, - showPaymentScreen = false, - showClaimHistory = false, - memberReminders = MemberReminders(), - ), - ) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `when payment-feature is activated, should show payment data`() = runTest { - val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() - val presenter = ProfilePresenter( - FakeGetEurobonusStatusUseCase().apply { - turbine.add(GetEurobonusError.EurobonusNotApplicable.left()) - }, - certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, - TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, - TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - fixedMap = mapOf( - Feature.PAYMENT_SCREEN to true, - Feature.HELP_CENTER to true, - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), - noopLogoutUseCase, - ) - - presenter.test(ProfileUiState.Loading) { - assertThat(awaitItem()).isInstanceOf() - runCurrent() - assertThat(awaitItem()).isEqualTo( - ProfileUiState.Success( - euroBonus = null, - certificatesAvailable = true, - showPaymentScreen = true, - showClaimHistory = false, - memberReminders = MemberReminders(), - ), - ) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `when payment-feature is activated, but response fails, should not show payment data`() = runTest { - val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() - val presenter = ProfilePresenter( - FakeGetEurobonusStatusUseCase().apply { - turbine.add(GetEurobonusError.EurobonusNotApplicable.left()) - }, - certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, - TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, - TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), - noopLogoutUseCase, - ) - - presenter.test(ProfileUiState.Loading) { - assertThat(awaitItem()).isInstanceOf() - runCurrent() - assertThat(awaitItem()).isEqualTo( - ProfileUiState.Success( - euroBonus = null, - certificatesAvailable = true, - showPaymentScreen = false, - showClaimHistory = false, - memberReminders = MemberReminders(), - ), - ) - cancelAndIgnoreRemainingEvents() - } - } - @Test fun `claims history feature flag hides the navigation option`( @TestParameter claimHistoryFlag: Boolean, @@ -155,7 +53,6 @@ class ProfilePresenterTest { TestEnableNotificationsReminderSnoozeManager(), FakeFeatureManager( mapOf( - Feature.PAYMENT_SCREEN to false, Feature.ENABLE_CLAIM_HISTORY to claimHistoryFlag, ), ), @@ -195,7 +92,7 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = null, certificatesAvailable = true, - showPaymentScreen = false, + showPaymentScreen = true, showClaimHistory = false, memberReminders = MemberReminders(), ), @@ -225,7 +122,7 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = true, - showPaymentScreen = false, + showPaymentScreen = true, showClaimHistory = false, memberReminders = MemberReminders(), ), @@ -254,7 +151,7 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = true, - showPaymentScreen = false, + showPaymentScreen = true, showClaimHistory = false, memberReminders = MemberReminders(), ), @@ -285,7 +182,7 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = false, - showPaymentScreen = false, + showPaymentScreen = true, showClaimHistory = false, memberReminders = MemberReminders(), ), @@ -298,8 +195,6 @@ class ProfilePresenterTest { fun `Initially all optional items are off, and as they come in, they show one by one`() = runTest { val featureManager = FakeFeatureManager( fixedMap = mapOf( - Feature.PAYMENT_SCREEN to true, - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to false, ), ) @@ -361,8 +256,6 @@ class ProfilePresenterTest { TestEnableNotificationsReminderSnoozeManager(), FakeFeatureManager( mapOf( - Feature.PAYMENT_SCREEN to false, - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to false, ), ), @@ -381,7 +274,7 @@ class ProfilePresenterTest { certificatesAvailable = false, memberReminders = MemberReminders(), showClaimHistory = false, - showPaymentScreen = false, + showPaymentScreen = true, ), ) @@ -402,8 +295,6 @@ class ProfilePresenterTest { TestEnableNotificationsReminderSnoozeManager(), FakeFeatureManager( mapOf( - Feature.PAYMENT_SCREEN to false, - Feature.HELP_CENTER to true, Feature.ENABLE_CLAIM_HISTORY to false, ), ), @@ -456,13 +347,12 @@ class ProfilePresenterTest { ) getMemberRemindersUseCase.memberReminders.add(MemberReminders()) getEurobonusStatusUseCase.turbine.add(GetEurobonusError.Error(ErrorMessage()).left()) - featureManager.featureTurbine.add(Feature.PAYMENT_SCREEN to false) runCurrent() assertThat(awaitItem()).isEqualTo( ProfileUiState.Success( euroBonus = null, certificatesAvailable = false, - showPaymentScreen = false, + showPaymentScreen = true, showClaimHistory = false, memberReminders = MemberReminders(), ), @@ -472,7 +362,6 @@ class ProfilePresenterTest { runCurrent() getEurobonusStatusUseCase.turbine.add(EuroBonus("abc").right()) certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) } - featureManager.featureTurbine.add(Feature.PAYMENT_SCREEN to true) getMemberRemindersUseCase.memberReminders.add( MemberReminders(connectPayment = MemberReminder.PaymentReminder.ConnectPayment(id = testId)), ) diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt index 23c40c4d75..6bdf8c69ca 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt @@ -2,16 +2,11 @@ package com.hedvig.android.featureflags.flags internal val Feature.unleashKey: String get() = when (this) { - Feature.DISABLE_CHAT -> "disable_chat" - Feature.MOVING_FLOW -> "moving_flow" - Feature.PAYMENT_SCREEN -> "payment_screen" + Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT -> "enable_new_conversation_from_inbox" Feature.TERMINATION_FLOW -> "disable_termination_flow" Feature.UPDATE_NECESSARY -> "update_necessary" - Feature.EDIT_COINSURED -> "edit_coinsured" - Feature.HELP_CENTER -> "disable_help_center" Feature.TRAVEL_ADDON -> "enable_addons" Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES -> "enable_video_player_in_chat_messages" - Feature.DISABLE_REDEEM_CAMPAIGN -> "disable_redeem_campaign" Feature.ENABLE_CLAIM_HISTORY -> "enable_claim_history" Feature.PUPPY_GUIDE -> "disable_puppy_guide" } diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index 9ca3feda85..df986ea469 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -16,18 +16,13 @@ internal class UnleashFeatureFlagProvider( when (feature) { // Kill switches: the remote toggle being on means the feature is off. Feature.TERMINATION_FLOW, - Feature.HELP_CENTER, Feature.PUPPY_GUIDE, -> !hedvigUnleashClient.client.isEnabled(key) Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT, - Feature.MOVING_FLOW, - Feature.PAYMENT_SCREEN, Feature.UPDATE_NECESSARY, - Feature.EDIT_COINSURED, Feature.TRAVEL_ADDON, Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES, - Feature.DISABLE_REDEEM_CAMPAIGN, Feature.ENABLE_CLAIM_HISTORY, -> hedvigUnleashClient.client.isEnabled(key) } diff --git a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt index 4af538fac0..19d5dca5f6 100644 --- a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -8,20 +8,14 @@ enum class Feature( "Enables inbox icon always available on the Home screen " + "and New conversation button inside the inbox", ), - @Suppress("ktlint:standard:max-line-length") - MOVING_FLOW("Lets a user change their address and get a new offer"), - PAYMENT_SCREEN("Controls whether the payment screen should be accessible from the profile tab"), TERMINATION_FLOW("Shows the button which enters the insurance termination flow from the insurance tab"), UPDATE_NECESSARY( "Defines the lowest supported app version. Should prompt a user to update if it uses an outdated version.", ), - EDIT_COINSURED("Let member edit co insured"), - HELP_CENTER("Enable the help center screens"), TRAVEL_ADDON("Let members purchase addons"), ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES( "When enabled, it allows the chat to show media in inline video players in the chat messages", ), - DISABLE_REDEEM_CAMPAIGN("Disables the ability to redeem a campaign code"), ENABLE_CLAIM_HISTORY("Enables claim history"), PUPPY_GUIDE( "Controls whether the puppy guide is available in the help center. Backed by the disable_puppy_guide kill switch.", diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetNeedsCoInsuredInfoRemindersUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetNeedsCoInsuredInfoRemindersUseCase.kt index 15f3e5a1a5..08a55b50f4 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetNeedsCoInsuredInfoRemindersUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetNeedsCoInsuredInfoRemindersUseCase.kt @@ -2,7 +2,6 @@ package com.hedvig.android.memberreminders import arrow.core.Either import arrow.core.NonEmptyList -import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull @@ -14,14 +13,10 @@ import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.data.coinsured.CoInsuredFlowType -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapLatest import octopus.NeedsCoInsuredInfoReminderQuery @@ -34,34 +29,25 @@ internal interface GetNeedsCoInsuredInfoRemindersUseCase { @Inject internal class GetNeedsCoInsuredInfoRemindersUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetNeedsCoInsuredInfoRemindersUseCase { override fun invoke(): Flow>> { - return featureManager.isFeatureEnabled(Feature.EDIT_COINSURED).flatMapLatest { isEditCoInsuredFeatureEnabled -> - if (!isEditCoInsuredFeatureEnabled) { - flow { - emit(CoInsuredInfoReminderError.CoInsuredReminderNotEnabled.left()) - } - } else { - apolloClient.query(NeedsCoInsuredInfoReminderQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .safeFlow(::ErrorMessage) - .mapLatest { result: Either -> - either { - val coInsuredReminderInfoList = result.mapLeft(CoInsuredInfoReminderError::NetworkError) - .bind() - .currentMember - .activeContracts - .toCoInsuredInfoList() - .toNonEmptyListOrNull() + return apolloClient.query(NeedsCoInsuredInfoReminderQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage) + .mapLatest { result: Either -> + either { + val coInsuredReminderInfoList = result.mapLeft(CoInsuredInfoReminderError::NetworkError) + .bind() + .currentMember + .activeContracts + .toCoInsuredInfoList() + .toNonEmptyListOrNull() - ensureNotNull(coInsuredReminderInfoList) { - CoInsuredInfoReminderError.NoCoInsuredReminders - } - } + ensureNotNull(coInsuredReminderInfoList) { + CoInsuredInfoReminderError.NoCoInsuredReminders } + } } - } } private fun List.toCoInsuredInfoList(): @@ -93,7 +79,5 @@ internal class GetNeedsCoInsuredInfoRemindersUseCaseImpl( sealed interface CoInsuredInfoReminderError { data object NoCoInsuredReminders : CoInsuredInfoReminderError - data object CoInsuredReminderNotEnabled : CoInsuredInfoReminderError - data class NetworkError(val errorMessage: ErrorMessage) : CoInsuredInfoReminderError, ErrorMessage by errorMessage } From 9e1299d925782f2cbcc7aa37b56464cc31731415 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Sat, 6 Jun 2026 01:31:35 +0300 Subject: [PATCH 4/5] Add critical info about metro KMP providers and navigateUp() usage --- CLAUDE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 8ffcf630be..d479f528a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -168,6 +168,14 @@ fun NavGraphBuilder.featureGraph( **Top-level navigation graphs:** Home, Insurances, Forever, Payments, Profile +**Critical navigation rule — `navigateUp` is reserved for the top app bar back button:** + +`backstack.navigateUp()` may **only** be wired to the back arrow in a screen's top app bar. In every other case — "done"/"close"/"continue" buttons, success screens, dismissing a flow, programmatic pops after an action — call `backstack.popBackstack()` instead. + +**Why:** `navigateUp` carries deep-link/synthetic-stack semantics (the `:app` `BackstackController` overrides it to rebuild a parent stack when the user arrived via a lone deep link). That behavior is correct for the top app bar's "up" affordance, but wrong for an in-content button, where the user expects a plain temporal pop of the current entry. Mixing them makes a button behave differently depending on how the screen was reached, and can diverge from predictive (system) back. + +**How to apply:** When arranging the backstack for a flow, do it *at navigation time* (when navigating to a screen), so that a later plain `popBackstack()` and the system back gesture always land in the same place — never special-case the pop inside a button handler. + ### Dependency Injection Uses Koin with modular configuration: @@ -196,6 +204,14 @@ val applicationModule = module { - Common dependencies (logging, tracking) auto-injected by build plugin - When a Presenter or ViewModel needs to call a use case, always inject the use case directly as a typed dependency — never abstract it into an anonymous `suspend () -> T` lambda. If two separate operations are needed (e.g. payin vs payout setup), create two separate, dedicated use case classes and two separate presenters. Do not create a shared interface just to enable reuse through a single presenter. +**Critical Metro KMP rule — never put a platform-overridable `@ContributesBinding` default in `commonMain`:** + +If an interface needs a different implementation per platform, bind it **per-platform** with explicit `@Provides`/`@ContributesBinding` in each platform source set (`androidMain`, `iosMain`/`nativeMain`, `jvmMain`) — the way `:featureflags:feature-flags` binds `FeatureManager` (`UnleashFeatureFlagProvider` on Android, provided via `FeatureFlagsAndroidMetroProviders`). Do **not** annotate a `commonMain` default impl with `@ContributesBinding`. + +**Why:** a `commonMain` `@ContributesBinding` contributes that binding to **every** target. A platform-specific impl (e.g. an `androidMain` class) that forgets its own contribution annotation is then **silently shadowed** by the common default at runtime — no compile error, just wrong behavior. This actually happened: `NoopPermissionManager` (commonMain, `isPermissionGranted` always `false`) shadowed the real `ActivityCompatPermissionManager` on Android, so every notification sender behaved as if `POST_NOTIFICATIONS` was never granted. With per-platform binding instead, a missing binding is a **compile-time** error (loud), not a silent fallback. + +**How to apply:** When you see a `commonMain` interface with platform-specific impls, bind per-platform and keep `commonMain` free of the default binding. If you must keep a `commonMain` default (Metro 1.1.1 has no `rank`), the platform override **must** carry `@ContributesBinding(AppScope::class, replaces = [TheCommonDefault::class])` — but prefer the per-platform pattern, since `replaces` only protects the impls that exist today and silently re-breaks if a future platform impl forgets to contribute. + ### Data Layer Data modules follow this structure when they are not KMP compatible: From 52fa0e29feccf29e3d5da833d41a2a52e308a2a2 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Mon, 22 Jun 2026 15:33:32 +0200 Subject: [PATCH 5/5] Cleanup old feature flags and more clearly note their default value Delete ENABLE_CLAIM_HISTORY, ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES, TRAVEL_ADDON, and TERMINATION_FLOW as we never turn those off Rework the remaining flags so each Feature enum name mirrors its Unleash key polarity (DISABLE_PUPPY_GUIDE, ENABLE_NEW_CONVERSATION_FROM_INBOX). UnleashFeatureFlagProvider now returns the raw isEnabled(key) for every flag, and kill-switch consumers invert at the read site. Due to the "default value" unleash problem, this means that each feature flag makes a more concious decision of what its "default" behavior is when we have failed to fetch it, depending on if it's a kill-switch style flag or a turn-on style flag. The `bootstrap` parameter in the `UnleashClient.start` function allows us to control that default ourselves instead before we ever get a proper first response from the backend --- CLAUDE.md | 8 +- .../addons/data/GetAddonBannerInfoUseCase.kt | 79 ++++++++----------- ...GetTravelAddonBannerInfoUseCaseImplTest.kt | 32 ++------ ...CreateChangeTierDeductibleIntentUseCase.kt | 7 +- ...angeTierDeductibleIntentUseCaseImplTest.kt | 20 ----- .../data/GetTerminatableContractsUseCase.kt | 24 +++--- .../purchase/data/GetAddonOfferUseCase.kt | 9 --- .../data/GetInsuranceForTravelAddonUseCase.kt | 27 ++----- ...tInsuranceForTravelAddonUseCaseImplTest.kt | 23 +----- .../GetTravelAddonOfferUseCaseImplTest.kt | 29 ++----- .../android/feature/chat/CbmChatViewModel.kt | 14 +--- .../feature/chat/inbox/InboxViewModel.kt | 2 +- .../data/DeleteAccountStateUseCase.kt | 14 +--- .../help/center/HelpCenterPresenter.kt | 6 +- .../center/puppyguide/PuppyGuideViewModel.kt | 12 +-- .../home/home/data/GetHomeDataUseCase.kt | 11 +-- .../home/home/data/GetHomeUseCaseTest.kt | 50 +++--------- .../data/GetInsuranceContractsUseCase.kt | 40 ++++------ .../ContractDetailViewModel.kt | 16 +--- .../GetInsuranceContractsUseCaseImplTest.kt | 18 +---- .../ContractDetailPresenterTest.kt | 74 ++++------------- .../AddHouseInformationViewModel.kt | 9 +-- .../EnterNewAddressViewModel.kt | 9 +-- .../feature/profile/tab/ProfileViewModel.kt | 9 +-- .../profile/tab/ProfilePresenterTest.kt | 76 ++---------------- .../feature-flags/FEATURE_FLAG_DEFAULTS.md | 42 +++++----- .../featureflags/HedvigUnleashClient.kt | 2 +- .../featureflags/flags/FeatureUnleashKey.kt | 8 +- .../flags/UnleashFeatureFlagProvider.kt | 20 +---- .../android/featureflags/flags/Feature.kt | 12 +-- 30 files changed, 183 insertions(+), 519 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d479f528a9..439e744dfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -493,10 +493,12 @@ the SDK's `defaultValue` parameter (Unleash Android SDK issue #141), how a flag' resolved when Unleash has never been fetched, and when bootstrap is required. To add a new flag: -1. Add the enum value to `Feature` (commonMain) with a short explanation. +1. Add the enum value to `Feature` (commonMain), named to mirror its Unleash key polarity + (`ENABLE_X` for `enable_x`, `DISABLE_X` for `disable_x`), with a short explanation. 2. Map it to its raw Unleash key in `Feature.unleashKey` (androidMain). -3. Add it to the correct arm in `UnleashFeatureFlagProvider`: positive `isEnabled(key)` or - kill switch `!isEnabled(key)`. +3. `UnleashFeatureFlagProvider` needs no change — it returns the raw `isEnabled(key)` for + every flag. At the read site, use the value directly for a positive flag, or invert it + (`if (!disableX)`) for a kill switch. **IMPORTANT — always reconsider bootstrap when adding a feature:** Decide what the flag should resolve to when it has *never been fetched* (offline first launch / fresh install diff --git a/app/data/data-addons/src/main/kotlin/com/hedvig/android/data/addons/data/GetAddonBannerInfoUseCase.kt b/app/data/data-addons/src/main/kotlin/com/hedvig/android/data/addons/data/GetAddonBannerInfoUseCase.kt index 7397b377df..4ed83c5807 100644 --- a/app/data/data-addons/src/main/kotlin/com/hedvig/android/data/addons/data/GetAddonBannerInfoUseCase.kt +++ b/app/data/data-addons/src/main/kotlin/com/hedvig/android/data/addons/data/GetAddonBannerInfoUseCase.kt @@ -11,14 +11,11 @@ import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.serialization.Serializable import octopus.AddonBannersQuery @@ -32,7 +29,6 @@ interface GetAddonBannerInfoUseCase { @Inject internal class GetAddonBannerInfoUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetAddonBannerInfoUseCase { override fun invoke(source: AddonBannerSource): Flow>> { val mappedSource = when (source) { @@ -49,59 +45,48 @@ internal class GetAddonBannerInfoUseCaseImpl( AddonBannerSource.CAR_ADDON_DEEPLINK -> listOf(AddonFlow.APP_CAR_PLUS) } - return combine( - featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON), - apolloClient - .query(AddonBannersQuery(mappedSource)) - .fetchPolicy(CacheAndNetwork) - .safeFlow() - .map { - it.mapLeft { error -> + return apolloClient + .query(AddonBannersQuery(mappedSource)) + .fetchPolicy(CacheAndNetwork) + .safeFlow() + .map { addonBannersQueryResult -> + either { + val bannerData = addonBannersQueryResult.mapLeft { error -> logcat(LogPriority.WARN, error) { "Error from AddonBannersQuery " + "from source: $mappedSource: $error" } ErrorMessage() + }.bind().currentMember.addonBanners + if (bannerData.isEmpty()) { + logcat(LogPriority.DEBUG) { "Got empty response from AddonBannersQuery" } + return@either emptyList() } - }, - ) { isAddonFlagOn, addonBannersQueryResult -> - either { - if (!isAddonFlagOn) { - logcat(LogPriority.INFO) { - "Tried AddonBannersQuery but travel addon feature flag is off" - } - return@either emptyList() - } - val bannerData = addonBannersQueryResult.bind().currentMember.addonBanners - if (bannerData.isEmpty()) { - logcat(LogPriority.DEBUG) { "Got empty response from AddonBannersQuery" } - return@either emptyList() - } - bannerData.mapNotNull { banner -> - val flowType = when (banner.flow) { - AddonFlow.APP_TRAVEL_PLUS_SELL_ONLY -> FlowType.APP_TRAVEL_PLUS_SELL_ONLY - AddonFlow.APP_TRAVEL_PLUS_SELL_OR_UPGRADE -> FlowType.APP_TRAVEL_PLUS_SELL_OR_UPGRADE - AddonFlow.APP_CAR_PLUS -> FlowType.APP_CAR_PLUS - AddonFlow.UNKNOWN__ -> null - } - val eligibleInsurancesIds = banner.contractIds.toNonEmptyListOrNull() - if (flowType == null || eligibleInsurancesIds == null) { - logcat(LogPriority.DEBUG) { - "Got AddonFlow.UNKNOWN or empty contractIds from AddonBannersQuery" + bannerData.mapNotNull { banner -> + val flowType = when (banner.flow) { + AddonFlow.APP_TRAVEL_PLUS_SELL_ONLY -> FlowType.APP_TRAVEL_PLUS_SELL_ONLY + AddonFlow.APP_TRAVEL_PLUS_SELL_OR_UPGRADE -> FlowType.APP_TRAVEL_PLUS_SELL_OR_UPGRADE + AddonFlow.APP_CAR_PLUS -> FlowType.APP_CAR_PLUS + AddonFlow.UNKNOWN__ -> null + } + val eligibleInsurancesIds = banner.contractIds.toNonEmptyListOrNull() + if (flowType == null || eligibleInsurancesIds == null) { + logcat(LogPriority.DEBUG) { + "Got AddonFlow.UNKNOWN or empty contractIds from AddonBannersQuery" + } + null + } else { + AddonBannerInfo( + title = banner.displayTitleName, + description = banner.descriptionDisplayName, + labels = banner.badges, + eligibleInsurancesIds = eligibleInsurancesIds, + flowType = flowType, + ) } - null - } else { - AddonBannerInfo( - title = banner.displayTitleName, - description = banner.descriptionDisplayName, - labels = banner.badges, - eligibleInsurancesIds = eligibleInsurancesIds, - flowType = flowType, - ) } } } - } } } diff --git a/app/data/data-addons/src/test/kotlin/GetTravelAddonBannerInfoUseCaseImplTest.kt b/app/data/data-addons/src/test/kotlin/GetTravelAddonBannerInfoUseCaseImplTest.kt index 95ded37d37..77f6cd6f25 100644 --- a/app/data/data-addons/src/test/kotlin/GetTravelAddonBannerInfoUseCaseImplTest.kt +++ b/app/data/data-addons/src/test/kotlin/GetTravelAddonBannerInfoUseCaseImplTest.kt @@ -16,8 +16,6 @@ import com.hedvig.android.data.addons.data.AddonBannerSource.INSURANCES_TAB import com.hedvig.android.data.addons.data.AddonBannerSource.TRAVEL_CERTIFICATES import com.hedvig.android.data.addons.data.FlowType import com.hedvig.android.data.addons.data.GetAddonBannerInfoUseCaseImpl -import com.hedvig.android.featureflags.flags.Feature.TRAVEL_ADDON -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -138,22 +136,9 @@ class GetTravelAddonBannerInfoUseCaseImplTest { ) } - @Test - fun `if FF for addons is off return empty list`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to false)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithTwoFlows, featureManager) - val resultFromInsurances = sut.invoke(INSURANCES_TAB).first() - assertThat(resultFromInsurances) - .isEqualTo(emptyList().right()) - val resultFromTravel = sut.invoke(TRAVEL_CERTIFICATES).first() - assertThat(resultFromTravel) - .isEqualTo(emptyList().right()) - } - @Test fun `if get null bannerData from BE return empty list`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithNullBannerData, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithNullBannerData) val result = sut.invoke(TRAVEL_CERTIFICATES).first() assertThat(result) .isEqualTo(emptyList().right()) @@ -161,8 +146,7 @@ class GetTravelAddonBannerInfoUseCaseImplTest { @Test fun `the source is mapped to the correct flow for the query`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithTwoFlows, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithTwoFlows) val resultFromTravelCertificates = sut.invoke(TRAVEL_CERTIFICATES).first().getOrNull() assertThat(resultFromTravelCertificates).isNotNull() val resultFromInsurances = sut.invoke(INSURANCES_TAB).first().getOrNull() @@ -171,8 +155,7 @@ class GetTravelAddonBannerInfoUseCaseImplTest { @Test fun `if get bannerData from BE is not null but contractIds are empty return empty list`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithEmptyContracts, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithEmptyContracts) val result = sut.invoke(TRAVEL_CERTIFICATES).first() assertThat(result) .isEqualTo(emptyList().right()) @@ -180,8 +163,7 @@ class GetTravelAddonBannerInfoUseCaseImplTest { @Test fun `if get error from BE return ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithError, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithError) val resultFromTravels = sut.invoke(TRAVEL_CERTIFICATES).first().isLeft() assertThat(resultFromTravels) .isTrue() @@ -189,8 +171,7 @@ class GetTravelAddonBannerInfoUseCaseImplTest { @Test fun `if get full banner data from BE return TravelAddonBannerInfo`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithFullBannerData, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithFullBannerData) val resultFromTravel = sut.invoke(TRAVEL_CERTIFICATES).first().getOrNull() assertThat(resultFromTravel) .isNotNull() @@ -198,8 +179,7 @@ class GetTravelAddonBannerInfoUseCaseImplTest { @Test fun `the received data is passed correctly and in full`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(TRAVEL_ADDON to true)) - val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithFullBannerData, featureManager) + val sut = GetAddonBannerInfoUseCaseImpl(apolloClientWithFullBannerData) val resultFromTravel = sut.invoke(TRAVEL_CERTIFICATES).first().getOrNull() assertThat(resultFromTravel) .isEqualTo( diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt index 83e870f800..709901bcde 100644 --- a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt @@ -10,8 +10,6 @@ import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.data.productvariant.toAddonVariant import com.hedvig.android.data.productvariant.toProductVariant -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.LogPriority.ERROR import com.hedvig.android.logger.logcat @@ -19,7 +17,6 @@ import com.hedvig.ui.tiersandaddons.CostBreakdownEntry import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn -import kotlinx.coroutines.flow.first import octopus.ChangeTierDeductibleCreateIntentMutation import octopus.fragment.DeductibleFragment import octopus.fragment.DisplayItemFragment @@ -37,20 +34,18 @@ internal interface CreateChangeTierDeductibleIntentUseCase { @Inject internal class CreateChangeTierDeductibleIntentUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : CreateChangeTierDeductibleIntentUseCase { override suspend fun invoke( insuranceId: String, source: ChangeTierCreateSource, ): Either { return either { - val isAddonFlagEnabled = featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).first() val changeTierDeductibleResponse = apolloClient .mutation( ChangeTierDeductibleCreateIntentMutation( contractId = insuranceId, source = source.toSource(), - addonsFlagOn = isAddonFlagEnabled, + addonsFlagOn = true, ), ) .safeExecute() diff --git a/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt b/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt index 6f2e186383..8ca4e29913 100644 --- a/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt +++ b/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt @@ -19,8 +19,6 @@ import com.hedvig.android.data.changetier.data.CreateChangeTierDeductibleIntentU import com.hedvig.android.data.changetier.data.IntentOutput import com.hedvig.android.data.changetier.data.TierConstants import com.hedvig.android.data.changetier.data.TierDeductibleQuote -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.test.runTest import kotlinx.datetime.LocalDate @@ -382,10 +380,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when BE response has empty quotes return intent with empty quotes`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodResponseButEmptyQuotes, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) assertk.assertThat(result) @@ -399,10 +395,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when response is fine get a good result`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodResponse, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -431,10 +425,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when response is otherwise good but the intent and deflect are null the result is ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodButNullResponse, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -446,10 +438,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when response is otherwise good but the tierName in existing agreement is null the result is ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodResponseButNullTierNameInExisting, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -461,10 +451,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when response is otherwise good but the tierName in one of the quotes is null the result is ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodResponseButNullTierNameInOneQuote, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -475,10 +463,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `in good response one of the quotes should have the current const id`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithGoodResponse, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) .getOrNull()?.intentOutput?.quotes?.filter { it.id == TierConstants.CURRENT_ID } @@ -497,10 +483,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when response is bad the result is ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithBadResponse, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -512,10 +496,8 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when result's deflectOutput is not null intentOutput should be null and deflectOutput should be populated`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithDeflectOutput, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) @@ -536,7 +518,6 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @Test fun `when deflectOutput is present intent field is ignored`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) val apolloClientWithBothSet = testApolloClientRule.apolloClient.apply { registerTestResponse( operation = ChangeTierDeductibleCreateIntentMutation( @@ -588,7 +569,6 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { val createChangeTierDeductibleIntentUseCase = CreateChangeTierDeductibleIntentUseCaseImpl( apolloClient = apolloClientWithBothSet, - featureManager = featureManager, ) val result = createChangeTierDeductibleIntentUseCase.invoke(testId, ChangeTierCreateSource.SELF_SERVICE) diff --git a/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt b/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt index 9b32e37131..b3ffeab978 100644 --- a/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt +++ b/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt @@ -13,13 +13,11 @@ import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.toContractGroup -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import octopus.ContractsToTerminateQuery interface GetTerminatableContractsUseCase { @@ -31,21 +29,17 @@ interface GetTerminatableContractsUseCase { @Inject internal class GetTerminatableContractsUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetTerminatableContractsUseCase { override suspend fun invoke(): Flow?>> { - return combine( - featureManager.isFeatureEnabled(Feature.TERMINATION_FLOW), - apolloClient - .query(ContractsToTerminateQuery()) - .fetchPolicy(FetchPolicy.NetworkOnly) - .safeFlow(::ErrorMessage), - ) { isEnabled, memberResponse -> - either { - if (!isEnabled) return@either null - memberResponse.bind().currentMember.toInsurancesForCancellation().toNonEmptyListOrNull() + return apolloClient + .query(ContractsToTerminateQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeFlow(::ErrorMessage) + .map { memberResponse -> + either { + memberResponse.bind().currentMember.toInsurancesForCancellation().toNonEmptyListOrNull() + } } - } } } diff --git a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetAddonOfferUseCase.kt b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetAddonOfferUseCase.kt index 80d5035cf4..efd4424807 100644 --- a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetAddonOfferUseCase.kt +++ b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetAddonOfferUseCase.kt @@ -13,14 +13,11 @@ import com.hedvig.android.data.productvariant.toAddonVariant import com.hedvig.android.data.productvariant.toProductVariant import com.hedvig.android.feature.addon.purchase.data.AddonOffer.Selectable import com.hedvig.android.feature.addon.purchase.data.AddonOffer.Toggleable -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn -import kotlinx.coroutines.flow.first import octopus.AddonGenerateOfferMutation import octopus.fragment.AddonOfferQuoteFragment import octopus.type.AddonDeflectType @@ -34,15 +31,9 @@ internal interface GetAddonOfferUseCase { @Inject internal class GetAddonOfferUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetAddonOfferUseCase { override suspend fun invoke(contractId: String): Either { return either { - val isAddonFlagOn = featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).first() - if (!isAddonFlagOn) { - logcat(LogPriority.ERROR) { "Tried to start AddonGenerateOffer but travel addon feature flag is off" } - raise(ErrorMessage()) - } apolloClient .mutation( AddonGenerateOfferMutation(contractId), diff --git a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetInsuranceForTravelAddonUseCase.kt b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetInsuranceForTravelAddonUseCase.kt index b99c7d5b42..c78f11cda6 100644 --- a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetInsuranceForTravelAddonUseCase.kt +++ b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/data/GetInsuranceForTravelAddonUseCase.kt @@ -12,15 +12,12 @@ import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.toContractGroup -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import octopus.InsurancesForTravelAddonQuery internal interface GetInsuranceForTravelAddonUseCase { @@ -32,23 +29,14 @@ internal interface GetInsuranceForTravelAddonUseCase { @Inject internal class GetInsuranceForTravelAddonUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetInsuranceForTravelAddonUseCase { override suspend fun invoke(ids: List): Flow>> { - return combine( - featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON), - apolloClient - .query(InsurancesForTravelAddonQuery()) - .fetchPolicy(FetchPolicy.NetworkOnly) - .safeFlow(::ErrorMessage), - ) { isEnabled, memberResponse -> - either { - if (!isEnabled) { - logcat(LogPriority.ERROR) { - "Tried to get list of insurances for addon purchase but the addon feature flag id off!" - } - raise(ErrorMessage()) - } else { + return apolloClient + .query(InsurancesForTravelAddonQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeFlow(::ErrorMessage) + .map { memberResponse -> + either { val result = memberResponse.bind().currentMember.toInsurancesForAddon(ids) ensure(result.isNotEmpty()) { logcat { "Tried to get list of insurances for addon purchase but the list is empty!" } @@ -57,7 +45,6 @@ internal class GetInsuranceForTravelAddonUseCaseImpl( result } } - } } } diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/data/GetInsuranceForTravelAddonUseCaseImplTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/data/GetInsuranceForTravelAddonUseCaseImplTest.kt index 686a628678..9e9ce0a9ce 100644 --- a/app/feature/feature-addon-purchase/src/test/kotlin/data/GetInsuranceForTravelAddonUseCaseImplTest.kt +++ b/app/feature/feature-addon-purchase/src/test/kotlin/data/GetInsuranceForTravelAddonUseCaseImplTest.kt @@ -16,8 +16,6 @@ import com.hedvig.android.core.common.test.isRight import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.feature.addon.purchase.data.GetInsuranceForTravelAddonUseCaseImpl import com.hedvig.android.feature.addon.purchase.data.InsuranceForAddon -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -98,19 +96,9 @@ class GetInsuranceForTravelAddonUseCaseImplTest { ) } - @Test - fun `if FF for addons is off return ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to false)) - val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodResponse, featureManager) - val result = sut.invoke(testIds).first() - assertThat(result) - .isLeft() - } - @Test fun `if quotes list is empty return ErrorMessage with null message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodButEmptyResponse, featureManager) + val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodButEmptyResponse) val result = sut.invoke(testIds).first() assertThat(result) .isLeft() @@ -119,8 +107,7 @@ class GetInsuranceForTravelAddonUseCaseImplTest { @Test fun `if BE gives error return ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithError, featureManager) + val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithError) val result = sut.invoke(testIds).first() assertThat(result) .isLeft() @@ -128,8 +115,7 @@ class GetInsuranceForTravelAddonUseCaseImplTest { @Test fun `if BE gives correct response but the required ids are not there return ErrorMessage`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodResponse, featureManager) + val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodResponse) val result = sut.invoke(listOf("someotherid")).first() assertThat(result) .isLeft() @@ -137,8 +123,7 @@ class GetInsuranceForTravelAddonUseCaseImplTest { @Test fun `if BE gives correct response and required ids are not there return correctly mapped list`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodResponse, featureManager) + val sut = GetInsuranceForTravelAddonUseCaseImpl(apolloClientWithGoodResponse) val result = sut.invoke(testIds).first() assertThat(result) .isRight().isEqualTo( diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/data/GetTravelAddonOfferUseCaseImplTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/data/GetTravelAddonOfferUseCaseImplTest.kt index d0295058db..e4c13a89df 100644 --- a/app/feature/feature-addon-purchase/src/test/kotlin/data/GetTravelAddonOfferUseCaseImplTest.kt +++ b/app/feature/feature-addon-purchase/src/test/kotlin/data/GetTravelAddonOfferUseCaseImplTest.kt @@ -4,7 +4,6 @@ import arrow.core.nonEmptyListOf import arrow.core.raise.either import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isNull import assertk.assertions.prop import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.annotations.ApolloExperimental @@ -29,8 +28,6 @@ import com.hedvig.android.feature.addon.purchase.data.GenerateAddonOfferResult import com.hedvig.android.feature.addon.purchase.data.GetAddonOfferUseCaseImpl import com.hedvig.android.feature.addon.purchase.data.TravelAddonQuoteInsuranceDocument import com.hedvig.android.feature.addon.purchase.data.UmbrellaAddonQuote -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.test.runTest import kotlinx.datetime.LocalDate @@ -405,19 +402,9 @@ class GetTravelAddonOfferUseCaseImplTest { ) } - @Test - fun `if FF for addons is off return ErrorMessage with null message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to false)) - val sut = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseWithCurrentAddon, featureManager) - val result = sut.invoke(testId) - assertThat(result) - .isLeft().prop(ErrorMessage::message).isNull() - } - @Test fun `if quotes list is empty return ErrorMessage with null message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseEmptyQuotes, featureManager) + val sut = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseEmptyQuotes) val result = sut.invoke(testId) assertThat(result) .isLeft() @@ -426,8 +413,7 @@ class GetTravelAddonOfferUseCaseImplTest { @Test fun `if BE gives error return ErrorMessage with null message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetAddonOfferUseCaseImpl(apolloClientWithError, featureManager) + val sut = GetAddonOfferUseCaseImpl(apolloClientWithError) val result = sut.invoke(testId) assertThat(result) .isLeft().prop(ErrorMessage::message).isEqualTo(null) @@ -435,8 +421,7 @@ class GetTravelAddonOfferUseCaseImplTest { @Test fun `if BE gives UserError return ErrorMessage with proper message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetAddonOfferUseCaseImpl(apolloClientWithUserError, featureManager) + val sut = GetAddonOfferUseCaseImpl(apolloClientWithUserError) val result = sut.invoke(testId) assertThat(result) .isLeft().prop(ErrorMessage::message).isEqualTo("You have 2 insurances") @@ -444,8 +429,7 @@ class GetTravelAddonOfferUseCaseImplTest { @Test fun `if BE gives data but it's null return ErrorMessage with null message`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut = GetAddonOfferUseCaseImpl(apolloClientWithNullData, featureManager) + val sut = GetAddonOfferUseCaseImpl(apolloClientWithNullData) val result = sut.invoke(testId).leftOrNull().toString() assertThat(result) .isEqualTo("ErrorMessage(message=null, throwable=null)") @@ -453,12 +437,11 @@ class GetTravelAddonOfferUseCaseImplTest { @Test fun `if BE gives full data map it correctly`() = runTest { - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TRAVEL_ADDON to true)) - val sut1 = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseNoCurrentAddon, featureManager) + val sut1 = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseNoCurrentAddon) val result1 = sut1.invoke(testId) assertThat(result1) .isEqualTo(either { mockWithoutUpgrade }) - val sut2 = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseWithCurrentAddon, featureManager) + val sut2 = GetAddonOfferUseCaseImpl(apolloClientWithFullResponseWithCurrentAddon) val result2 = sut2.invoke(testId) assertThat(result2) .isEqualTo(either { mockWithUpgrade }) diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/CbmChatViewModel.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/CbmChatViewModel.kt index 8c1cd13d15..cf0c1e0ae3 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/CbmChatViewModel.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/CbmChatViewModel.kt @@ -49,8 +49,6 @@ import com.hedvig.android.feature.chat.model.Sender import com.hedvig.android.feature.chat.model.toChatMessage import com.hedvig.android.feature.chat.model.toLatestChatMessage import com.hedvig.android.feature.chat.paging.ChatRemoteMediator -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope @@ -73,7 +71,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -85,7 +82,6 @@ internal class CbmChatViewModel @AssistedInject constructor( chatDao: ChatDao, remoteKeyDao: RemoteKeyDao, chatRepository: Provider, - featureManager: FeatureManager, clock: Clock, context: Context, coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + AndroidUiDispatcher.Main), @@ -104,7 +100,6 @@ internal class CbmChatViewModel @AssistedInject constructor( ), chatDao = chatDao, chatRepository = chatRepository, - featureManager = featureManager, context, ), coroutineScope = coroutineScope, @@ -155,7 +150,6 @@ internal class CbmChatPresenter( private val pagingData: Flow>, private val chatDao: ChatDao, private val chatRepository: Provider, - private val featureManager: FeatureManager, private val context: Context, ) : MoleculePresenter { @OptIn(ExperimentalPagingApi::class) @@ -176,13 +170,7 @@ internal class CbmChatPresenter( var hideBanner by remember { mutableStateOf(false) } var showFileFailedToBeSendToast by remember { mutableStateOf(false) } val a11yOn = isAccessibilityEnabled(context) - val enableInlineMediaPlayer by remember(featureManager) { - if (!a11yOn) { - featureManager.isFeatureEnabled(Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES) - } else { - flowOf(false) - } - }.collectAsState(false) + val enableInlineMediaPlayer = !a11yOn LaunchedEffect(conversationIdStatusLoadIteration) { if (conversationInfoStatus is Loaded && conversationIdStatusLoadIteration == 0) { diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt index ae0052f203..d5db68c0be 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt @@ -59,7 +59,7 @@ internal class InboxPresenter( } combine( getAllConversationsUseCase.invoke(), - featureManager.isFeatureEnabled(Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT), + featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), ) { conversations, newChatButtonAvailable -> conversations to newChatButtonAvailable }.collectLatest { (conversations, newChatButtonAvailable) -> diff --git a/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/data/DeleteAccountStateUseCase.kt b/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/data/DeleteAccountStateUseCase.kt index 755ac69443..62f1b67193 100644 --- a/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/data/DeleteAccountStateUseCase.kt +++ b/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/data/DeleteAccountStateUseCase.kt @@ -6,13 +6,10 @@ import com.apollographql.apollo.cache.normalized.FetchPolicy import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest import octopus.DeleteAccountStateQuery import octopus.type.ClaimStatus @@ -21,17 +18,14 @@ import octopus.type.ClaimStatus internal class DeleteAccountStateUseCase( private val apolloClient: ApolloClient, private val deleteAccountRequestStorage: DeleteAccountRequestStorage, - private val featureManager: FeatureManager, ) { suspend fun invoke(): Flow { return combine( deleteAccountRequestStorage.hasRequestedTermination(), - featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_HISTORY).flatMapLatest { enableClaimHistory -> - apolloClient.query( - DeleteAccountStateQuery(enableClaimHistory), - ).fetchPolicy(FetchPolicy.CacheAndNetwork).safeFlow { - DeleteAccountState.NetworkError - } + apolloClient.query( + DeleteAccountStateQuery(true), + ).fetchPolicy(FetchPolicy.CacheAndNetwork).safeFlow { + DeleteAccountState.NetworkError }, ) { hasRequestedTermination, queryResponse -> if (hasRequestedTermination) { diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt index 6bee76b99b..e8d5d2c4b0 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt @@ -174,8 +174,8 @@ internal class HelpCenterPresenter( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, flow3 = getPuppyGuideUseCase.invoke(), - flow4 = featureManager.isFeatureEnabled(Feature.PUPPY_GUIDE), - ) { quickLinks, faq, puppyGuideResult, puppyGuideEnabled -> + flow4 = featureManager.isFeatureEnabled(Feature.DISABLE_PUPPY_GUIDE), + ) { quickLinks, faq, puppyGuideResult, puppyGuideDisabled -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -195,7 +195,7 @@ internal class HelpCenterPresenter( val questions = faq.getOrNull()?.commonFAQ ?: listOf() val puppyGuide = puppyGuideResult.getOrNull() val puppyGuidePresentation = when { - !puppyGuideEnabled -> null + puppyGuideDisabled -> null puppyGuide == null || puppyGuide.stories.isEmpty() -> null puppyGuide.isForYoungDog == true -> HelpCenterUiState.PuppyGuidePresentation.FullCard else -> HelpCenterUiState.PuppyGuidePresentation.QuickAction diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt index 89c7f71e61..80a4857359 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt @@ -43,8 +43,8 @@ private class PuppyGuidePresenter( override fun MoleculePresenterScope.present(lastState: PuppyGuideUiState): PuppyGuideUiState { var currentState by remember { mutableStateOf(lastState) } var loadIteration by remember { mutableIntStateOf(0) } - val puppyGuideEnabled by remember(featureManager) { - featureManager.isFeatureEnabled(Feature.PUPPY_GUIDE) + val puppyGuideDisabled by remember(featureManager) { + featureManager.isFeatureEnabled(Feature.DISABLE_PUPPY_GUIDE) }.collectAsState(null) CollectEvents { event -> @@ -53,18 +53,18 @@ private class PuppyGuidePresenter( } } - LaunchedEffect(loadIteration, puppyGuideEnabled) { - when (puppyGuideEnabled) { + LaunchedEffect(loadIteration, puppyGuideDisabled) { + when (puppyGuideDisabled) { // Flag not resolved yet, keep showing the loading state. null -> { currentState = PuppyGuideUiState.Loading } - false -> { + true -> { currentState = PuppyGuideUiState.Disabled } - true -> { + false -> { currentState = PuppyGuideUiState.Loading getPuppyGuideUseCase.invoke().collect { response -> currentState = response.fold( diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index 6c96581a10..fa49eadddd 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.datetime.LocalDate @@ -64,11 +63,9 @@ internal class GetHomeDataUseCaseImpl( ) : GetHomeDataUseCase { override fun invoke(forceNetworkFetch: Boolean): Flow> { return combine( - featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_HISTORY).flatMapLatest { enableClaimHistory -> - apolloClient.query(HomeQuery(enableClaimHistory)) - .fetchPolicy(if (forceNetworkFetch) FetchPolicy.NetworkOnly else FetchPolicy.CacheAndNetwork) - .safeFlow() - }, + apolloClient.query(HomeQuery(true)) + .fetchPolicy(if (forceNetworkFetch) FetchPolicy.NetworkOnly else FetchPolicy.CacheAndNetwork) + .safeFlow(), flow { while (currentCoroutineContext().isActive) { emitAll( @@ -83,7 +80,7 @@ internal class GetHomeDataUseCaseImpl( flow { emitAll(getTravelAddonBannerInfoUseCaseProvider.provide().invoke(AddonBannerSource.INSURANCES_TAB)) }, - featureManager.isFeatureEnabled(Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT), + featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), ) { homeQueryDataResult, diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt index 14443a8217..a6bcdb9784 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt @@ -245,22 +245,14 @@ internal class GetHomeUseCaseTest { } @Test - fun `when there are existing claims, show them as ClaimStatusCards`( - @TestParameter claimsHistoryFlag: Boolean, - ) = runTest { - val getHomeDataUseCase = testUseCaseWithoutReminders( - featureManager = FakeFeatureManager( - fixedMap = Feature.entries.associateWith { true }.plus( - Feature.ENABLE_CLAIM_HISTORY to claimsHistoryFlag, - ), - ), - ) + fun `when there are existing claims, show them as ClaimStatusCards`() = runTest { + val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(claimsHistoryFlag), + HomeQuery(true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { - val claimsList = listOf( + claimsActive = listOf( buildClaim { id = "claim id#1" }, @@ -268,11 +260,6 @@ internal class GetHomeUseCaseTest { id = "claim id#2" }, ) - if (!claimsHistoryFlag) { - claims = claimsList - } else { - claimsActive = claimsList - } } }, ) @@ -300,26 +287,14 @@ internal class GetHomeUseCaseTest { } @Test - fun `when there are no existing claims, don't show them`( - @TestParameter claimsHistoryFlag: Boolean, - ) = runTest { - val getHomeDataUseCase = testUseCaseWithoutReminders( - featureManager = FakeFeatureManager( - fixedMap = Feature.entries.associateWith { true }.plus( - Feature.ENABLE_CLAIM_HISTORY to claimsHistoryFlag, - ), - ), - ) + fun `when there are no existing claims, don't show them`() = runTest { + val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(claimsHistoryFlag), + HomeQuery(true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { - if (claimsHistoryFlag) { - claimsActive = emptyList() - } else { - claims = emptyList() - } + claimsActive = emptyList() } }, ) @@ -478,9 +453,8 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.ENABLE_CLAIM_HISTORY to true, // With the inbox-always-available kill switch off, the icon depends purely on existing conversations - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to false, + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) @@ -542,8 +516,7 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.ENABLE_CLAIM_HISTORY to true, - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to inboxAlwaysAvailable, + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to inboxAlwaysAvailable, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) @@ -602,9 +575,8 @@ internal class GetHomeUseCaseTest { ) = runTest { val featureManager = FakeFeatureManager( mapOf( - Feature.ENABLE_CLAIM_HISTORY to true, // Inbox-always-available off, so the icon reflects the conversation state being tested here - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT to false, + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt index deb989aa69..ae15795ff3 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt @@ -20,8 +20,6 @@ import com.hedvig.android.data.productvariant.toAddonVariant import com.hedvig.android.data.productvariant.toProductVariant import com.hedvig.android.feature.insurances.data.InsuranceContract.EstablishedInsuranceContract import com.hedvig.android.feature.insurances.data.InsuranceContract.PendingInsuranceContract -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn @@ -30,7 +28,6 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive @@ -51,30 +48,27 @@ internal interface GetInsuranceContractsUseCase { @SingleIn(AppScope::class) internal class GetInsuranceContractsUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : GetInsuranceContractsUseCase { override fun invoke(): Flow>> { - return featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).flatMapLatest { areAddonsEnabled -> - flow { - while (currentCoroutineContext().isActive) { - emitAll( - apolloClient - .query( - InsuranceContractsQuery( - addonsEnabled = areAddonsEnabled, - options = Optional.present( - DisplayItemOptions( - hidePrice = Optional.present(true), - hideAddons = Optional.present(true), - ), + return flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient + .query( + InsuranceContractsQuery( + addonsEnabled = true, + options = Optional.present( + DisplayItemOptions( + hidePrice = Optional.present(true), + hideAddons = Optional.present(true), ), ), - ) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .safeFlow(::ErrorMessage), - ) - delay(3.seconds) - } + ), + ) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage), + ) + delay(3.seconds) } }.map { insuranceQueryResponse -> either { diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailViewModel.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailViewModel.kt index 778d1c8a51..a043ed5102 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailViewModel.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailViewModel.kt @@ -11,8 +11,6 @@ import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.feature.insurances.data.InsuranceContract import com.hedvig.android.feature.insurances.data.InsuranceContract.EstablishedInsuranceContract import com.hedvig.android.feature.insurances.insurancedetail.GetContractForContractIdUseCaseImpl.GetContractForContractIdError -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -22,16 +20,14 @@ import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey -import kotlinx.coroutines.flow.combine @AssistedInject internal class ContractDetailViewModel( @Assisted contractId: String, - featureManager: FeatureManager, getContractForContractIdUseCase: GetContractForContractIdUseCase, ) : MoleculeViewModel( initialState = ContractDetailsUiState.Loading, - presenter = ContractDetailPresenter(contractId, featureManager, getContractForContractIdUseCase), + presenter = ContractDetailPresenter(contractId, getContractForContractIdUseCase), ) { @AssistedFactory @ManualViewModelAssistedFactoryKey @@ -45,7 +41,6 @@ internal class ContractDetailViewModel( internal class ContractDetailPresenter( private val contractId: String, - private val featureManager: FeatureManager, private val getContractForContractIdUseCase: GetContractForContractIdUseCase, ) : MoleculePresenter { @@ -66,12 +61,7 @@ internal class ContractDetailPresenter( if (currentState !is ContractDetailsUiState.Success) { currentState = ContractDetailsUiState.Loading } - combine( - getContractForContractIdUseCase.invoke(contractId), - featureManager.isFeatureEnabled(Feature.TERMINATION_FLOW), - ) { insuranceContractResult, isTerminationFlowEnabled -> - insuranceContractResult to isTerminationFlowEnabled - }.collect { (insuranceContractResult, isTerminationFlowEnabled) -> + getContractForContractIdUseCase.invoke(contractId).collect { insuranceContractResult -> insuranceContractResult.fold( ifLeft = { error -> currentState = when (error) { @@ -82,7 +72,7 @@ internal class ContractDetailPresenter( ifRight = { contract -> currentState = ContractDetailsUiState.Success( insuranceContract = contract, - allowTerminatingInsurance = isTerminationFlowEnabled && contract.supportsTermination, + allowTerminatingInsurance = contract.supportsTermination, ) }, ) diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt index 63ddc6094b..9eefa48bfc 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseImplTest.kt @@ -11,8 +11,6 @@ import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver import com.hedvig.android.apollo.test.TestApolloClientRule import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.test.isRight -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -40,7 +38,7 @@ class GetInsuranceContractsUseCaseImplTest { get() = testApolloClientRule.apolloClient.apply { registerTestResponse( operation = InsuranceContractsQuery( - false, + true, Optional.Present( DisplayItemOptions( Optional.Present(true), @@ -68,7 +66,7 @@ class GetInsuranceContractsUseCaseImplTest { get() = testApolloClientRule.apolloClient.apply { registerTestResponse( operation = InsuranceContractsQuery( - false, + true, Optional.Present( DisplayItemOptions( Optional.Present(true), @@ -123,14 +121,8 @@ class GetInsuranceContractsUseCaseImplTest { @Test fun `when the contract response has isChangeTierEnabled as true, InsuranceContract should have supportsTierChange as true`() = runTest { - val featureManager = FakeFeatureManager( - fixedMap = mapOf( - Feature.TRAVEL_ADDON to false, - ), - ) val subjectUseCase = GetInsuranceContractsUseCaseImpl( apolloClient = apolloClientWithGoodResponseThatSupportsTier, - featureManager = featureManager, ) val result = subjectUseCase.invoke().first() assertThat(result).isRight().transform { @@ -145,14 +137,8 @@ class GetInsuranceContractsUseCaseImplTest { @Test fun `when the contract response has isChangeTierEnabled as false InsuranceContract should have supportsTierChange as false`() = runTest { - val featureManager = FakeFeatureManager( - fixedMap = mapOf( - Feature.TRAVEL_ADDON to false, - ), - ) val subjectUseCase = GetInsuranceContractsUseCaseImpl( apolloClient = apolloClientWithGoodResponseWithoutTier, - featureManager = featureManager, ) val result = subjectUseCase.invoke().first() assertThat(result).isRight().transform { diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt index 6d1a45e76a..5a61a63375 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt @@ -18,8 +18,6 @@ import com.hedvig.android.feature.insurances.data.InsuranceAgreement import com.hedvig.android.feature.insurances.data.InsuranceContract.EstablishedInsuranceContract import com.hedvig.android.feature.insurances.data.MonthlyCost import com.hedvig.android.feature.insurances.insurancedetail.GetContractForContractIdUseCaseImpl.GetContractForContractIdError -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule import com.hedvig.android.molecule.test.test import kotlinx.coroutines.flow.Flow @@ -34,12 +32,10 @@ class ContractDetailPresenterTest { val testLogcatLogger = TestLogcatLoggingRule() @Test - fun `if termination flow enabled and no termination date show cancel insurance button`() = runTest { + fun `if contract supports termination and no termination date show cancel insurance button`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test(ContractDetailsUiState.Loading) { @@ -58,10 +54,8 @@ class ContractDetailPresenterTest { fun `with an initial success, if there is an error, can retry and get back in success state again through loading`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test( @@ -83,10 +77,8 @@ class ContractDetailPresenterTest { @Test fun `with an initial error state, can retry and with a good response get success state through loading`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test( @@ -103,10 +95,8 @@ class ContractDetailPresenterTest { @Test fun `with an initial error state, if a good response comes with the flow, show success state`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test( @@ -122,10 +112,8 @@ class ContractDetailPresenterTest { @Test fun `with an initial success state do not show loading if get the same response`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) val successState = ContractDetailsUiState.Success( @@ -145,10 +133,8 @@ class ContractDetailPresenterTest { @Test fun `with an initial success state do not show loading if get the different successful response`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) val successStateFirst = ContractDetailsUiState.Success( @@ -168,34 +154,11 @@ class ContractDetailPresenterTest { } } - @Test - fun `if termination flow disabled not show cancel insurance button`() = runTest { - val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to false)) - val presenter = ContractDetailPresenter( - contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, - getContractForContractIdUseCase = getContractForContractIdUseCase, - ) - presenter.test(ContractDetailsUiState.Loading) { - assertThat(awaitItem()).isEqualTo(ContractDetailsUiState.Loading) - getContractForContractIdUseCase.addInsuranceWithNoTerminationDateToResponseTurbine() - assertThat(awaitItem()).isEqualTo( - ContractDetailsUiState.Success( - allowTerminatingInsurance = false, - insuranceContract = getContractForContractIdUseCase.getInsuranceWithOutTerminationDate(), - ), - ) - } - } - @Test fun `if contractId is wrong show no contract found state`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to false)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getInvalidId(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test(ContractDetailsUiState.Loading) { @@ -210,10 +173,8 @@ class ContractDetailPresenterTest { @Test fun `if contractId is okay show success state`() = runTest { val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to false)) val presenter = ContractDetailPresenter( contractId = getContractForContractIdUseCase.getValIdWithoutTerminationDate(), - featureManager = featureManager, getContractForContractIdUseCase = getContractForContractIdUseCase, ) presenter.test(ContractDetailsUiState.Loading) { @@ -224,26 +185,23 @@ class ContractDetailPresenterTest { } @Test - fun `if termination is enabled but contract does not support termination not show cancel insurance button`() = - runTest { - val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() - val featureManager = FakeFeatureManager(fixedMap = mapOf(Feature.TERMINATION_FLOW to true)) - val presenter = ContractDetailPresenter( - contractId = getContractForContractIdUseCase.getValIdWithTerminationDate(), - featureManager = featureManager, - getContractForContractIdUseCase = getContractForContractIdUseCase, + fun `if contract does not support termination not show cancel insurance button`() = runTest { + val getContractForContractIdUseCase = FakeGetContractForContractIdUseCase() + val presenter = ContractDetailPresenter( + contractId = getContractForContractIdUseCase.getValIdWithTerminationDate(), + getContractForContractIdUseCase = getContractForContractIdUseCase, + ) + presenter.test(ContractDetailsUiState.Loading) { + assertThat(awaitItem()).isEqualTo(ContractDetailsUiState.Loading) + getContractForContractIdUseCase.addInsuranceWithTerminationDateToResponseTurbine() + assertThat(awaitItem()).isEqualTo( + ContractDetailsUiState.Success( + insuranceContract = getContractForContractIdUseCase.getInsuranceWithTerminationDate(), + allowTerminatingInsurance = false, + ), ) - presenter.test(ContractDetailsUiState.Loading) { - assertThat(awaitItem()).isEqualTo(ContractDetailsUiState.Loading) - getContractForContractIdUseCase.addInsuranceWithTerminationDateToResponseTurbine() - assertThat(awaitItem()).isEqualTo( - ContractDetailsUiState.Success( - insuranceContract = getContractForContractIdUseCase.getInsuranceWithTerminationDate(), - allowTerminatingInsurance = false, - ), - ) - } } + } internal class FakeGetContractForContractIdUseCase : GetContractForContractIdUseCase { private val insuranceWithNoTerminationDate = EstablishedInsuranceContract( diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/addhouseinformation/AddHouseInformationViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/addhouseinformation/AddHouseInformationViewModel.kt index 1878219acb..dc2f41eb1e 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/addhouseinformation/AddHouseInformationViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/addhouseinformation/AddHouseInformationViewModel.kt @@ -34,8 +34,6 @@ import com.hedvig.android.feature.movingflow.ui.addhouseinformation.AddHouseInfo import com.hedvig.android.feature.movingflow.ui.addhouseinformation.AddHouseInformationUiState.Content.SubmittingInfoFailure.NetworkFailure import com.hedvig.android.feature.movingflow.ui.addhouseinformation.AddHouseInformationUiState.Loading import com.hedvig.android.feature.movingflow.ui.addhouseinformation.AddHouseInformationUiState.MissingOngoingMovingFlow -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -45,7 +43,6 @@ import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import octopus.feature.movingflow.MoveIntentV2RequestMutation import octopus.type.MoveExtraBuildingInput @@ -58,14 +55,12 @@ internal class AddHouseInformationViewModel( @Assisted moveIntentId: String, movingFlowRepository: MovingFlowRepository, apolloClient: ApolloClient, - featureManager: FeatureManager, ) : MoleculeViewModel( Loading, AddHouseInformationPresenter( moveIntentId, movingFlowRepository, apolloClient, - featureManager, ), ) { @AssistedFactory @@ -82,7 +77,6 @@ internal class AddHouseInformationPresenter( private val moveIntentId: String, private val movingFlowRepository: MovingFlowRepository, private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present( @@ -145,13 +139,12 @@ internal class AddHouseInformationPresenter( LaunchedEffect(inputForSubmission) { @Suppress("NAME_SHADOWING") val inputForSubmissionValue = inputForSubmission ?: return@LaunchedEffect - val isAddonFlagEnabled = featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).first() apolloClient .mutation( MoveIntentV2RequestMutation( intentId = moveIntentId, moveIntentRequestInput = inputForSubmissionValue.moveIntentRequestInput, - addonsFlagOn = isAddonFlagEnabled, + addonsFlagOn = true, ), ) .safeExecute() diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt index 5c9b40304b..d3c9b6d310 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt @@ -50,8 +50,6 @@ import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressV import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressValidationError.InvalidPostalCode.Missing import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressValidationError.InvalidPostalCode.MustBeOnlyDigits import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressValidationError.InvalidSquareMeters -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -62,7 +60,6 @@ import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import octopus.feature.movingflow.MoveIntentV2RequestMutation @@ -77,14 +74,12 @@ internal class EnterNewAddressViewModel( @Assisted moveIntentId: String, movingFlowRepository: MovingFlowRepository, apolloClient: ApolloClient, - featureManager: FeatureManager, ) : MoleculeViewModel( Loading, EnterNewAddressPresenter( moveIntentId, movingFlowRepository, apolloClient, - featureManager, ), ) { @AssistedFactory @@ -101,7 +96,6 @@ private class EnterNewAddressPresenter( private val moveIntentId: String, private val movingFlowRepository: MovingFlowRepository, private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present( @@ -176,13 +170,12 @@ private class EnterNewAddressPresenter( LaunchedEffect(inputForSubmission) { @Suppress("NAME_SHADOWING") val inputForSubmissionValue = inputForSubmission ?: return@LaunchedEffect - val isAddonFlagEnabled = featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).first() apolloClient .mutation( MoveIntentV2RequestMutation( intentId = moveIntentId, moveIntentRequestInput = inputForSubmissionValue.moveIntentRequestInput, - addonsFlagOn = isAddonFlagEnabled, + addonsFlagOn = true, ), ) .safeExecute() diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt index 2adb51064d..5a886ff2ae 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt @@ -14,8 +14,6 @@ import com.hedvig.android.feature.profile.data.CheckCertificatesAvailabilityUseC import com.hedvig.android.feature.profile.tab.ProfileUiEvent.Logout import com.hedvig.android.feature.profile.tab.ProfileUiEvent.Reload import com.hedvig.android.feature.profile.tab.ProfileUiEvent.SnoozeNotificationPermission -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.memberreminders.EnableNotificationsReminderSnoozeManager import com.hedvig.android.memberreminders.GetMemberRemindersUseCase import com.hedvig.android.memberreminders.MemberReminders @@ -38,7 +36,6 @@ internal class ProfileViewModel( checkCertificatesAvailabilityUseCase: CheckCertificatesAvailabilityUseCase, getMemberRemindersUseCase: GetMemberRemindersUseCase, enableNotificationsReminderSnoozeManager: EnableNotificationsReminderSnoozeManager, - featureManager: FeatureManager, logoutUseCase: LogoutUseCase, ) : MoleculeViewModel( initialState = ProfileUiState.Loading, @@ -47,7 +44,6 @@ internal class ProfileViewModel( checkCertificatesAvailabilityUseCase = checkCertificatesAvailabilityUseCase, getMemberRemindersUseCase = getMemberRemindersUseCase, enableNotificationsReminderSnoozeManager = enableNotificationsReminderSnoozeManager, - featureManager = featureManager, logoutUseCase = logoutUseCase, ), ) @@ -57,7 +53,6 @@ internal class ProfilePresenter( private val checkCertificatesAvailabilityUseCase: CheckCertificatesAvailabilityUseCase, private val getMemberRemindersUseCase: GetMemberRemindersUseCase, private val enableNotificationsReminderSnoozeManager: EnableNotificationsReminderSnoozeManager, - private val featureManager: FeatureManager, private val logoutUseCase: LogoutUseCase, ) : MoleculePresenter { @Composable @@ -80,12 +75,10 @@ internal class ProfilePresenter( } combine( getMemberRemindersUseCase.invoke(), - featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_HISTORY), flow { emit(getEuroBonusStatusUseCase.invoke()) }, flow { emit(checkCertificatesAvailabilityUseCase.invoke()) }, ) { memberReminders, - isClaimHistoryFeatureEnabled, eurobonusResponse, certificatesAvailability, -> @@ -93,7 +86,7 @@ internal class ProfilePresenter( euroBonus = eurobonusResponse.getOrNull(), showPaymentScreen = true, memberReminders = memberReminders, - showClaimHistory = isClaimHistoryFeatureEnabled, + showClaimHistory = true, certificatesAvailable = certificatesAvailability.isRight(), ) }.collectLatest { state -> diff --git a/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt b/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt index 4ff70f5e0e..2d9d8e39c9 100644 --- a/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt +++ b/app/feature/feature-profile/src/test/kotlin/com/hedvig/android/feature/profile/tab/ProfilePresenterTest.kt @@ -7,19 +7,12 @@ import arrow.core.right import assertk.assertAll import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull -import assertk.assertions.isTrue -import assertk.assertions.prop -import com.google.testing.junit.testparameterinjector.TestParameter -import com.google.testing.junit.testparameterinjector.TestParameterInjector import com.hedvig.android.auth.LogoutUseCase import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.test.MainCoroutineRule import com.hedvig.android.feature.profile.data.CheckCertificatesAvailabilityUseCase -import com.hedvig.android.featureflags.flags.Feature -import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.memberreminders.MemberReminder import com.hedvig.android.memberreminders.MemberReminders import com.hedvig.android.memberreminders.test.TestEnableNotificationsReminderSnoozeManager @@ -29,9 +22,7 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(TestParameterInjector::class) class ProfilePresenterTest { @get:Rule val mainCoroutineRule = MainCoroutineRule() @@ -42,35 +33,6 @@ class ProfilePresenterTest { } } - @Test - fun `claims history feature flag hides the navigation option`( - @TestParameter claimHistoryFlag: Boolean, - ) = runTest { - val presenter = ProfilePresenter( - FakeGetEurobonusStatusUseCase().apply { turbine.add(GetEurobonusError.EurobonusNotApplicable.left()) }, - FakeCheckCertificatesAvailabilityUseCase().apply { turbine.add(Unit.right()) }, - TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, - TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - mapOf( - Feature.ENABLE_CLAIM_HISTORY to claimHistoryFlag, - ), - ), - noopLogoutUseCase, - ) - - presenter.test(ProfileUiState.Loading) { - assertThat(awaitItem()).isInstanceOf() - runCurrent() - assertThat(awaitItem()) - .isInstanceOf() - .prop(ProfileUiState.Success::showClaimHistory) - .run { - if (claimHistoryFlag) isTrue() else isFalse() - } - } - } - @Test fun `when euro bonus does not exist, should not show the EuroBonus status`() = runTest { val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() @@ -81,7 +43,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) @@ -93,7 +54,7 @@ class ProfilePresenterTest { euroBonus = null, certificatesAvailable = true, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -111,7 +72,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) @@ -123,7 +83,7 @@ class ProfilePresenterTest { euroBonus = EuroBonus("code1234"), certificatesAvailable = true, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -141,7 +101,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) presenter.test(ProfileUiState.Loading) { @@ -152,7 +111,7 @@ class ProfilePresenterTest { euroBonus = EuroBonus("code1234"), certificatesAvailable = true, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -172,7 +131,6 @@ class ProfilePresenterTest { }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) presenter.test(ProfileUiState.Loading) { @@ -183,7 +141,7 @@ class ProfilePresenterTest { euroBonus = EuroBonus("code1234"), certificatesAvailable = false, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -193,11 +151,6 @@ class ProfilePresenterTest { @Test fun `Initially all optional items are off, and as they come in, they show one by one`() = runTest { - val featureManager = FakeFeatureManager( - fixedMap = mapOf( - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ) val euroBonusStatusUseCase = FakeGetEurobonusStatusUseCase() val getMemberRemindersUseCase = TestGetMemberRemindersUseCase() val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() @@ -207,7 +160,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - featureManager, noopLogoutUseCase, ) @@ -227,7 +179,7 @@ class ProfilePresenterTest { upcomingRenewals = null, enableNotifications = null, ), - showClaimHistory = false, + showClaimHistory = true, showPaymentScreen = true, ), ) @@ -254,11 +206,6 @@ class ProfilePresenterTest { }, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - mapOf( - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), noopLogoutUseCase, ) @@ -273,7 +220,7 @@ class ProfilePresenterTest { euroBonus = null, certificatesAvailable = false, memberReminders = MemberReminders(), - showClaimHistory = false, + showClaimHistory = true, showPaymentScreen = true, ), ) @@ -293,11 +240,6 @@ class ProfilePresenterTest { }, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - mapOf( - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), noopLogoutUseCase, ) @@ -328,13 +270,11 @@ class ProfilePresenterTest { val getMemberRemindersUseCase = TestGetMemberRemindersUseCase() val getEurobonusStatusUseCase = FakeGetEurobonusStatusUseCase() val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() - val featureManager = FakeFeatureManager(mapOf(Feature.ENABLE_CLAIM_HISTORY to false)) val presenter = ProfilePresenter( getEurobonusStatusUseCase, certificatesAvailabilityUseCase, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - featureManager, noopLogoutUseCase, ) val testId = "test" @@ -353,7 +293,7 @@ class ProfilePresenterTest { euroBonus = null, certificatesAvailable = false, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -371,7 +311,7 @@ class ProfilePresenterTest { euroBonus = EuroBonus("abc"), certificatesAvailable = true, showPaymentScreen = true, - showClaimHistory = false, + showClaimHistory = true, memberReminders = MemberReminders( connectPayment = MemberReminder.PaymentReminder.ConnectPayment(id = testId), ), diff --git a/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md b/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md index 8a1dcf06c6..be03cc3689 100644 --- a/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md +++ b/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md @@ -10,10 +10,12 @@ returns, etc.). Read this before adding a new flag. - We **only** call `client.isEnabled(name)`. We **never** call the `isEnabled(name, defaultValue)` overload — it's broken with the Frontend API. - An absent toggle reads as `false`. We control the real default through two levers: - 1. **Flag naming polarity** (`enable_x` vs `disable_x`) + explicit negation at the - read site in `UnleashFeatureFlagProvider`. + 1. **Flag naming polarity** (`enable_x` vs `disable_x`). Each `Feature` enum value is + named to mirror its underlying Unleash key, and `UnleashFeatureFlagProvider` returns + the raw `isEnabled(key)` value with no per-flag negation. A `disable_x` flag therefore + reports "is the kill switch on"; the consumer inverts at the read site. 2. **Bootstrap** — only for the one flag where polarity alone gives the wrong default. -- Only `PUPPY_GUIDE` is bootstrapped today. Adding others is usually noise and, for +- Only `DISABLE_PUPPY_GUIDE` is bootstrapped today. Adding others is usually noise and, for app-gating flags like `UPDATE_NECESSARY`, actively dangerous. ## The bug: Unleash Android SDK issue #141 @@ -30,17 +32,20 @@ hasn't seen. As long as we never pass a `defaultValue`, we're not exposed to #14 ## How a flag resolves to a value -`isEnabled(name)` returns `false` for an absent toggle. We turn that into a -feature-enabled boolean in `UnleashFeatureFlagProvider`, choosing the polarity per flag: +`isEnabled(name)` returns `false` for an absent toggle. `UnleashFeatureFlagProvider` +returns that raw value unchanged — the `Feature` name mirrors the key's polarity, so the +toggle value *is* the flag value. The polarity convention then determines the default: -- **Positive flags** (`enable_x`, `payment_screen`, `moving_flow`, `update_necessary`…) - read `isEnabled(key)` directly. Absent → `false` → feature **off**. Good default for - new features: they stay off until we explicitly turn them on remotely. +- **Positive flags** (`enable_x`, `update_necessary`…) read `isEnabled(key)`. Absent → + `false` → feature **off**. Good default for new features: they stay off until we + explicitly turn them on remotely. -- **Kill switches** (`disable_x`) read `!isEnabled(key)`. Absent → `true` → feature - **on**. The feature is normally available, and the remote toggle is a switch we flip to - turn it *off*. When offline we can't fetch the switch, so the feature stays on — that's - an inherent and acceptable property of a kill switch. +- **Kill switches** (`disable_x`) also read `isEnabled(key)`, which reports "is the kill + switch on". Absent → `false` → switch **off** → feature **on**. The consumer inverts at + the read site (`if (!disableX)`), so the feature is normally available and the remote + toggle is a switch we flip to turn it *off*. When offline we can't fetch the switch, so + it stays off and the feature stays on — an inherent and acceptable property of a kill + switch. ## When the "never fetched" default actually matters @@ -63,7 +68,7 @@ Bootstrap is only needed when the **desired** never-fetched default differs from Today the only entry is: ```kotlin -client.start(bootstrap = listOf(Toggle(name = Feature.PUPPY_GUIDE.unleashKey, enabled = true))) +client.start(bootstrap = listOf(Toggle(name = Feature.DISABLE_PUPPY_GUIDE.unleashKey, enabled = true))) ``` `disable_puppy_guide` is a kill switch, so its natural absent default is "feature on". @@ -81,11 +86,12 @@ is offline on first launch. Leave it alone. ## Adding a new flag — checklist -1. Add the enum value to `Feature` (commonMain) with a short explanation. -2. Add its raw Unleash key to `Feature.unleashKey` (androidMain). -3. Add it to the correct arm in `UnleashFeatureFlagProvider`: - - positive `isEnabled(key)`, or - - kill switch `!isEnabled(key)`. +1. Add the enum value to `Feature` (commonMain), named to mirror its Unleash key polarity + (`ENABLE_X` for `enable_x`, `DISABLE_X` for `disable_x`), with a short explanation. +2. Add its raw Unleash key to `Feature.unleashKey` (androidMain). `UnleashFeatureFlagProvider` + needs no change — it returns `isEnabled(key)` for every flag. +3. At the read site, use the value directly for a positive flag, or invert it + (`if (!disableX)`) for a kill switch. 4. Ask: **what should this be when never fetched / offline on first launch?** - If the natural polarity default is acceptable → done, no bootstrap. - If you need the opposite default during rollout → add a `Toggle(...)` to the diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt index 39138162f0..0521494edd 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt @@ -71,7 +71,7 @@ class HedvigUnleashClient( } // Bootstrap the puppy guide kill switch to on, so the feature stays hidden until the first // successful fetch. Once toggles are fetched, the remote value takes over. - client.start(bootstrap = listOf(Toggle(name = Feature.PUPPY_GUIDE.unleashKey, enabled = true))) + client.start(bootstrap = listOf(Toggle(name = Feature.DISABLE_PUPPY_GUIDE.unleashKey, enabled = true))) } private fun createConfig(): UnleashConfig { diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt index 6bdf8c69ca..365edbcc1b 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt @@ -2,11 +2,7 @@ package com.hedvig.android.featureflags.flags internal val Feature.unleashKey: String get() = when (this) { - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT -> "enable_new_conversation_from_inbox" - Feature.TERMINATION_FLOW -> "disable_termination_flow" + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX -> "enable_new_conversation_from_inbox" Feature.UPDATE_NECESSARY -> "update_necessary" - Feature.TRAVEL_ADDON -> "enable_addons" - Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES -> "enable_video_player_in_chat_messages" - Feature.ENABLE_CLAIM_HISTORY -> "enable_claim_history" - Feature.PUPPY_GUIDE -> "disable_puppy_guide" + Feature.DISABLE_PUPPY_GUIDE -> "disable_puppy_guide" } diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index df986ea469..423115c52a 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -10,22 +10,10 @@ internal class UnleashFeatureFlagProvider( private val hedvigUnleashClient: HedvigUnleashClient, ) : FeatureManager { override fun isFeatureEnabled(feature: Feature): Flow { + // Each Feature's name mirrors the polarity of its underlying Unleash key (enable_* / disable_*), + // so the raw toggle value is the feature value. Callers of a disable_* flag invert at the read site. return hedvigUnleashClient.featureUpdatedFlow - .map { - val key = feature.unleashKey - when (feature) { - // Kill switches: the remote toggle being on means the feature is off. - Feature.TERMINATION_FLOW, - Feature.PUPPY_GUIDE, - -> !hedvigUnleashClient.client.isEnabled(key) - - Feature.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT, - Feature.UPDATE_NECESSARY, - Feature.TRAVEL_ADDON, - Feature.ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES, - Feature.ENABLE_CLAIM_HISTORY, - -> hedvigUnleashClient.client.isEnabled(key) - } - }.distinctUntilChanged() + .map { hedvigUnleashClient.client.isEnabled(feature.unleashKey) } + .distinctUntilChanged() } } diff --git a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt index 19d5dca5f6..38a8d870ed 100644 --- a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -4,20 +4,14 @@ enum class Feature( // Used to easier get a context of what it's for. @Suppress("unused") val explanation: String, ) { - ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT( + ENABLE_NEW_CONVERSATION_FROM_INBOX( "Enables inbox icon always available on the Home screen " + "and New conversation button inside the inbox", ), - TERMINATION_FLOW("Shows the button which enters the insurance termination flow from the insurance tab"), UPDATE_NECESSARY( "Defines the lowest supported app version. Should prompt a user to update if it uses an outdated version.", ), - TRAVEL_ADDON("Let members purchase addons"), - ENABLE_VIDEO_PLAYER_IN_CHAT_MESSAGES( - "When enabled, it allows the chat to show media in inline video players in the chat messages", - ), - ENABLE_CLAIM_HISTORY("Enables claim history"), - PUPPY_GUIDE( - "Controls whether the puppy guide is available in the help center. Backed by the disable_puppy_guide kill switch.", + DISABLE_PUPPY_GUIDE( + "Kill switch for the puppy guide in the help center. When the toggle is on, the puppy guide is hidden.", ), }