diff --git a/CLAUDE.md b/CLAUDE.md index c704ff86fa..6409b9493f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -235,6 +235,16 @@ backstack.removeAllOf() Never hold a long-lived reference to `entries` snapshot contents; mutate through the controller/extensions so changes are observed and persisted. +**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 + **Registering destinations.** Each feature exposes a `fun EntryProviderScope.featureEntries(...)` that calls `entry { }` for each of its screens. `:app` calls all of them from `hedvigEntryProvider`. Cross-feature navigation is done by `:app` passing `navigateToX` lambdas into each feature's entries function — features never import each other's keys. ```kotlin @@ -256,6 +266,14 @@ fun EntryProviderScope.insuranceEntries(backstack: Backstack, /* n - `SessionReconciler.kt` — auth↔back-stack reconciliation; gates the splash via `isReady`; forced logout. - `HedvigEntryProvider.kt` — all destination registration and cross-feature lambda wiring. +**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. + ### Deep Links Each feature builds `DeepLinkMatcher`s from its `HedvigDeepLinkContainer` patterns and contributes a `DeepLinkMatcherProvider` (`@ContributesIntoSet`). `:app` aggregates them into one `HedvigDeepLinkMatcher`. `MainActivity` forwards `ACTION_VIEW` intents as raw URI strings down a `deepLinkChannel`; `HedvigApp` matches each to a key and routes it through the controller once logged in. A `DeepLinkAncestry` key entered while logged out is held as `pendingDeepLink` and landed after login. @@ -530,6 +548,28 @@ dependencies { 3. Build generates type-safe Kotlin code. 4. Use the generated query in an internal repository/use case impl; return a project-owned type. +### 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), 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. `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 +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..b13ad2e696 --- /dev/null +++ b/FEATURE_FLAG_CLEANUP_NOTES.md @@ -0,0 +1,80 @@ +# 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) — 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: +- 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/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 40aeca51c2..e71df908e1 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 @@ -50,8 +50,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 @@ -70,7 +68,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 @@ -83,7 +80,6 @@ internal class CbmChatViewModel @AssistedInject constructor( chatDao: ChatDao, remoteKeyDao: RemoteKeyDao, chatRepository: Provider, - featureManager: FeatureManager, clock: Clock, context: Context, coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + AndroidUiDispatcher.Main), @@ -102,7 +98,6 @@ internal class CbmChatViewModel @AssistedInject constructor( ), chatDao = chatDao, chatRepository = chatRepository, - featureManager = featureManager, context, ), coroutineScope = coroutineScope, @@ -144,7 +139,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) @@ -165,13 +159,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 4e23339e55..b66564ecd5 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 @@ -55,7 +55,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 90283e4bea..0c168b1253 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 @@ -45,6 +45,8 @@ import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDes import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkTermination import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate 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.feature.help.center.navigation.EmergencyKey import com.hedvig.android.feature.help.center.navigation.FirstVetKey import com.hedvig.android.feature.movingflow.SelectContractForMovingKey @@ -124,6 +126,7 @@ internal class HelpCenterPresenter( private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val featureManager: FeatureManager, private val backstack: Backstack, ) : MoleculePresenter { @Composable @@ -242,7 +245,8 @@ internal class HelpCenterPresenter( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, flow3 = getPuppyGuideUseCase.invoke(), - ) { quickLinks, faq, puppyGuideResult -> + flow4 = featureManager.isFeatureEnabled(Feature.DISABLE_PUPPY_GUIDE), + ) { quickLinks, faq, puppyGuideResult, puppyGuideDisabled -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -262,6 +266,7 @@ internal class HelpCenterPresenter( val questions = faq.getOrNull()?.commonFAQ ?: listOf() val puppyGuide = puppyGuideResult.getOrNull() val puppyGuidePresentation = when { + 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/HelpCenterViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt index 34c2c14f88..bbc67a6f2a 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 @@ -6,6 +6,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 import com.hedvig.android.navigation.compose.Backstack import dev.zacsweers.metro.Inject @@ -17,6 +18,7 @@ internal class HelpCenterViewModel( hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, getPuppyGuideUseCase: GetPuppyGuideUseCase, + featureManager: FeatureManager, backstack: Backstack, ) : MoleculeViewModel( initialState = HelpCenterUiState( @@ -32,6 +34,7 @@ internal class HelpCenterViewModel( getQuickLinksUseCase = getQuickLinksUseCase, hasAnyActiveConversationUseCase = hasAnyActiveConversationUseCase, getHelpCenterFAQUseCase = getHelpCenterFAQUseCase, + featureManager = featureManager, getPuppyGuideUseCase = getPuppyGuideUseCase, backstack = backstack, ), 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/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 f27e80ccdf..f6f0f542ec 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 @@ -90,6 +90,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 @@ -134,6 +135,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( @@ -522,5 +531,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 a4b0ab2d78..6dc8a4e4ee 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 @@ -11,6 +12,8 @@ import com.hedvig.android.core.common.di.ActivityRetainedScope import com.hedvig.android.core.common.di.HedvigViewModel 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 @@ -21,19 +24,24 @@ import kotlinx.coroutines.flow.SharingStarted @HedvigViewModel(ActivityRetainedScope::class) 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 puppyGuideDisabled by remember(featureManager) { + featureManager.isFeatureEnabled(Feature.DISABLE_PUPPY_GUIDE) + }.collectAsState(null) CollectEvents { event -> when (event) { @@ -41,12 +49,26 @@ private class PuppyGuidePresenter( } } - LaunchedEffect(loadIteration) { - getPuppyGuideUseCase.invoke().collect { response -> - currentState = response.fold( - ifLeft = { PuppyGuideUiState.Failure }, - ifRight = { puppyGuide -> PuppyGuideUiState.Success(puppyGuide.stories) }, - ) + LaunchedEffect(loadIteration, puppyGuideDisabled) { + when (puppyGuideDisabled) { + // Flag not resolved yet, keep showing the loading state. + null -> { + currentState = PuppyGuideUiState.Loading + } + + true -> { + currentState = PuppyGuideUiState.Disabled + } + + false -> { + currentState = PuppyGuideUiState.Loading + getPuppyGuideUseCase.invoke().collect { response -> + currentState = response.fold( + ifLeft = { PuppyGuideUiState.Failure }, + ifRight = { puppyGuide -> PuppyGuideUiState.Success(puppyGuide.stories) }, + ) + } + } } } @@ -64,4 +86,6 @@ internal sealed interface PuppyGuideUiState { data object Loading : PuppyGuideUiState data object Failure : PuppyGuideUiState + + data object Disabled : PuppyGuideUiState } diff --git a/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt b/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt index 7389c2e71e..ae158b2bac 100644 --- a/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt +++ b/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.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..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,15 +80,13 @@ 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), + featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), ) { homeQueryDataResult, unreadMessageCountResult, memberReminders, travelBannerInfo, - isHelpCenterEnabled, inboxAlwaysAvailable, anyActiveConversations, -> @@ -173,7 +168,7 @@ internal class GetHomeDataUseCaseImpl( veryImportantMessages = veryImportantMessages, memberReminders = memberReminders, hasUnseenChatMessages = hasUnseenChatMessages, - showHelpCenter = isHelpCenterEnabled, + showHelpCenter = true, firstVetSections = firstVetActions, crossSells = crossSells, travelBannerInfo = travelBannerInfo?.firstOrNull(), @@ -325,16 +320,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 +337,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..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,10 +453,8 @@ 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, + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) @@ -543,9 +516,7 @@ 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, + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to inboxAlwaysAvailable, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) @@ -597,47 +568,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,10 +575,8 @@ 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, + 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 47066cc6ef..11bbff6011 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 @@ -29,10 +27,9 @@ 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 @@ -51,49 +48,41 @@ internal interface GetInsuranceContractsUseCase { @SingleIn(AppScope::class) internal class GetInsuranceContractsUseCaseImpl( private val apolloClient: ApolloClient, - 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 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) - } - } - }, - featureManager.isFeatureEnabled(Feature.EDIT_COINSURED), - featureManager.isFeatureEnabled(Feature.MOVING_FLOW), - ) { insuranceQueryResponse, isEditCoInsuredEnabled, isMovingFlowFlagEnabled -> + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage), + ) + delay(3.seconds) + } + }.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 +92,6 @@ internal class GetInsuranceContractsUseCaseImpl( isTerminated = false, contractHolderDisplayName = contractHolderDisplayName, contractHolderSSN = contractHolderSSN, - isEditCoInsuredEnabled = isEditCoInsuredEnabled, isMovingFlowEnabled = isMovingEnabledForMember, ) } @@ -155,7 +143,6 @@ private fun ContractFragment.toContract( isTerminated: Boolean, contractHolderDisplayName: String, contractHolderSSN: String?, - isEditCoInsuredEnabled: Boolean, isMovingFlowEnabled: Boolean, ): EstablishedInsuranceContract { return EstablishedInsuranceContract( @@ -209,8 +196,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/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt index 6d6fff47a3..29df5515bc 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt @@ -287,7 +287,7 @@ private fun ContractDetailScreen( ) { pageIndex -> when (pageIndex) { 0 -> { - val priceInfoForBottomSheet = if (cost!=null) { + val priceInfoForBottomSheet = if (cost != null) { PriceInfoForBottomSheet( displayItems = buildList { add( @@ -300,9 +300,9 @@ private fun ContractDetailScreen( add( addon.addonVariant.displayName to stringResource( - Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, - addon.premium.toString(), - ), + Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, + addon.premium.toString(), + ), ) } cost.discounts.forEach { discount -> @@ -312,7 +312,9 @@ private fun ContractDetailScreen( totalGross = cost.monthlyGross, totalNet = cost.monthlyNet, ) - } else null + } else { + null + } YourInfoTab( contractId = contract.id, coverageItems = contract.displayItems, @@ -355,10 +357,12 @@ private fun ContractDetailScreen( contractHolderDisplayName = contract.contractHolderDisplayName, contractHolderSSN = contract.contractHolderSSN, priceToShow = cost?.monthlyNet, - showPriceInfoIcon = cost!=null && (cost.monthlyNet != cost.monthlyGross || - contract.basePremium != cost.monthlyNet), + showPriceInfoIcon = cost != null && ( + cost.monthlyNet != cost.monthlyGross || + contract.basePremium != cost.monthlyNet + ), onInfoIconClick = { - if (priceInfoForBottomSheet!=null) { + if (priceInfoForBottomSheet != null) { costBreakdownBottomSheetState.show(priceInfoForBottomSheet) } }, 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 23fb8c7832..b9191103ae 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 @@ -10,31 +10,25 @@ import androidx.compose.runtime.setValue import com.hedvig.android.core.common.di.ActivityRetainedScope import com.hedvig.android.core.common.di.HedvigViewModel 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 import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject -import kotlinx.coroutines.flow.combine @AssistedInject @HedvigViewModel(ActivityRetainedScope::class) internal class ContractDetailViewModel( @Assisted contractId: String, - featureManager: FeatureManager, getContractForContractIdUseCase: GetContractForContractIdUseCase, ) : MoleculeViewModel( initialState = ContractDetailsUiState.Loading, - presenter = ContractDetailPresenter(contractId, featureManager, getContractForContractIdUseCase), + presenter = ContractDetailPresenter(contractId, getContractForContractIdUseCase), ) internal class ContractDetailPresenter( private val contractId: String, - private val featureManager: FeatureManager, private val getContractForContractIdUseCase: GetContractForContractIdUseCase, ) : MoleculePresenter { @@ -55,12 +49,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) { @@ -71,7 +60,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/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt index c511c649c7..653ce7c214 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt @@ -74,7 +74,7 @@ internal fun UpcomingChangesBottomSheetContent( onInfoIconClick = { priceInfoBottomSheetState.show(it) }, - isFirstRow = sections.isEmpty() + isFirstRow = sections.isEmpty(), ) } Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt index 636435fa57..937adc7a5e 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt @@ -702,8 +702,8 @@ internal fun PriceRow( ) { HorizontalItemsWithMaximumSpaceTaken( modifier = modifier - .then ( - if (isFirstRow) Modifier else Modifier.horizontalDivider(DividerPosition.Top) + .then( + if (isFirstRow) Modifier else Modifier.horizontalDivider(DividerPosition.Top), ), startSlot = { Row( 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..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,17 +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.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, - Feature.TRAVEL_ADDON to false, - ), - ) val subjectUseCase = GetInsuranceContractsUseCaseImpl( apolloClient = apolloClientWithGoodResponseThatSupportsTier, - featureManager = featureManager, ) val result = subjectUseCase.invoke().first() assertThat(result).isRight().transform { @@ -148,17 +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.MOVING_FLOW to true, - Feature.EDIT_COINSURED to true, - Feature.PAYMENT_SCREEN to true, - 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 aa255f3a75..161f3aa8e4 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 @@ -35,8 +35,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,6 +43,9 @@ import com.hedvig.android.navigation.compose.add import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory 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 @@ -59,7 +60,6 @@ internal class AddHouseInformationViewModel( @Assisted moveIntentId: String, movingFlowRepository: MovingFlowRepository, apolloClient: ApolloClient, - featureManager: FeatureManager, backstack: Backstack, ) : MoleculeViewModel( Loading, @@ -67,7 +67,6 @@ internal class AddHouseInformationViewModel( moveIntentId, movingFlowRepository, apolloClient, - featureManager, backstack, ), ) @@ -76,7 +75,6 @@ internal class AddHouseInformationPresenter( private val moveIntentId: String, private val movingFlowRepository: MovingFlowRepository, private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, private val backstack: Backstack, ) : MoleculePresenter { @Composable @@ -135,13 +133,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 5e34cd642f..b0191ad647 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 @@ -51,8 +51,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.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import octopus.feature.movingflow.MoveIntentV2RequestMutation @@ -78,7 +75,6 @@ internal class EnterNewAddressViewModel( @Assisted moveIntentId: String, movingFlowRepository: MovingFlowRepository, apolloClient: ApolloClient, - featureManager: FeatureManager, backstack: Backstack, ) : MoleculeViewModel( Loading, @@ -86,7 +82,6 @@ internal class EnterNewAddressViewModel( moveIntentId, movingFlowRepository, apolloClient, - featureManager, backstack, ), ) @@ -95,7 +90,6 @@ private class EnterNewAddressPresenter( private val moveIntentId: String, private val movingFlowRepository: MovingFlowRepository, private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, private val backstack: Backstack, ) : MoleculePresenter { @Composable @@ -161,13 +155,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 f4598362e5..4f24d79138 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 @@ -34,7 +32,6 @@ internal class ProfileViewModel( checkCertificatesAvailabilityUseCase: CheckCertificatesAvailabilityUseCase, getMemberRemindersUseCase: GetMemberRemindersUseCase, enableNotificationsReminderSnoozeManager: EnableNotificationsReminderSnoozeManager, - featureManager: FeatureManager, logoutUseCase: LogoutUseCase, ) : MoleculeViewModel( initialState = ProfileUiState.Loading, @@ -43,7 +40,6 @@ internal class ProfileViewModel( checkCertificatesAvailabilityUseCase = checkCertificatesAvailabilityUseCase, getMemberRemindersUseCase = getMemberRemindersUseCase, enableNotificationsReminderSnoozeManager = enableNotificationsReminderSnoozeManager, - featureManager = featureManager, logoutUseCase = logoutUseCase, ), ) @@ -53,7 +49,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 @@ -76,22 +71,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, + 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 fceebd00f7..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,138 +33,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, - ) = 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.PAYMENT_SCREEN to false, - 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() @@ -184,7 +43,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) @@ -195,8 +53,8 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = null, certificatesAvailable = true, - showPaymentScreen = false, - showClaimHistory = false, + showPaymentScreen = true, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -214,7 +72,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase.apply { turbine.add(Unit.right()) }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) @@ -225,8 +82,8 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = true, - showPaymentScreen = false, - showClaimHistory = false, + showPaymentScreen = true, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -244,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) { @@ -254,8 +110,8 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = true, - showPaymentScreen = false, - showClaimHistory = false, + showPaymentScreen = true, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -275,7 +131,6 @@ class ProfilePresenterTest { }, TestGetMemberRemindersUseCase().apply { memberReminders.add(MemberReminders()) }, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager(fixedReturnForAll = false), noopLogoutUseCase, ) presenter.test(ProfileUiState.Loading) { @@ -285,8 +140,8 @@ class ProfilePresenterTest { ProfileUiState.Success( euroBonus = EuroBonus("code1234"), certificatesAvailable = false, - showPaymentScreen = false, - showClaimHistory = false, + showPaymentScreen = true, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -296,13 +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.PAYMENT_SCREEN to true, - Feature.HELP_CENTER to true, - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ) val euroBonusStatusUseCase = FakeGetEurobonusStatusUseCase() val getMemberRemindersUseCase = TestGetMemberRemindersUseCase() val certificatesAvailabilityUseCase = FakeCheckCertificatesAvailabilityUseCase() @@ -312,7 +160,6 @@ class ProfilePresenterTest { certificatesAvailabilityUseCase, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - featureManager, noopLogoutUseCase, ) @@ -332,7 +179,7 @@ class ProfilePresenterTest { upcomingRenewals = null, enableNotifications = null, ), - showClaimHistory = false, + showClaimHistory = true, showPaymentScreen = true, ), ) @@ -359,13 +206,6 @@ class ProfilePresenterTest { }, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - mapOf( - Feature.PAYMENT_SCREEN to false, - Feature.HELP_CENTER to true, - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), noopLogoutUseCase, ) @@ -380,8 +220,8 @@ class ProfilePresenterTest { euroBonus = null, certificatesAvailable = false, memberReminders = MemberReminders(), - showClaimHistory = false, - showPaymentScreen = false, + showClaimHistory = true, + showPaymentScreen = true, ), ) @@ -400,13 +240,6 @@ class ProfilePresenterTest { }, getMemberRemindersUseCase, TestEnableNotificationsReminderSnoozeManager(), - FakeFeatureManager( - mapOf( - Feature.PAYMENT_SCREEN to false, - Feature.HELP_CENTER to true, - Feature.ENABLE_CLAIM_HISTORY to false, - ), - ), noopLogoutUseCase, ) @@ -437,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" @@ -456,14 +287,13 @@ 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, - showClaimHistory = false, + showPaymentScreen = true, + showClaimHistory = true, memberReminders = MemberReminders(), ), ) @@ -472,7 +302,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)), ) @@ -482,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 new file mode 100644 index 0000000000..be03cc3689 --- /dev/null +++ b/app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md @@ -0,0 +1,98 @@ +# 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`). 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 `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 + +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. `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`, `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`) 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 + +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.DISABLE_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), 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 + 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..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 @@ -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.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 new file mode 100644 index 0000000000..365edbcc1b --- /dev/null +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt @@ -0,0 +1,8 @@ +package com.hedvig.android.featureflags.flags + +internal val Feature.unleashKey: String + get() = when (this) { + Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX -> "enable_new_conversation_from_inbox" + Feature.UPDATE_NECESSARY -> "update_necessary" + 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 0c75387ba2..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,35 +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 { - when (feature) { - 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.ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT -> hedvigUnleashClient.client.isEnabled( - "enable_new_conversation_from_inbox", - ) - } - }.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 33e8e6ed43..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,23 +4,14 @@ enum class Feature( // Used to easier get a context of what it's for. @Suppress("unused") val explanation: String, ) { - @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"), + ENABLE_NEW_CONVERSATION_FROM_INBOX( + "Enables inbox icon always available on the Home screen " + + "and New conversation button inside the inbox", + ), 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"), - ALWAYS_AVAILABLE_INBOX_AND_NEW_CHAT( - "Enables inbox icon always available on the Home screen " + - "and New conversation button inside the inbox", + DISABLE_PUPPY_GUIDE( + "Kill switch for the puppy guide in the help center. When the toggle is on, the puppy guide is hidden.", ), } 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 }