From f794585596c8b0371a89e31a5f83c5d8f26a2979 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 28 Sep 2026 10:58:20 -0400 Subject: [PATCH] chore(flags): retire ContactPickerMode and FrostedTipCard FrostedTipCard has had no reader since #1220 removed the frosted backdrop and left the flag in place. ContactPickerMode is retired as off, which is what every user already gets. The picker path lost its only entry point when direct-send went (#1419), so turning it on would skip the contacts permission step and leave the reader with an empty picker store. ScopeAwareContactReader now always reads through full access and still falls back to picked contacts when that fails; the picker reader and launcher stay for a future entry point. That reader was the contacts module's only FeatureFlag reference, so `:apps:flipcash:shared:featureflags` leaves its build file. Both DataStore keys stay behind unread, the same as the other retired flags. --- .../app/login/OnboardingFlowScreen.kt | 18 ++++----------- .../flipcash/shared/contacts/build.gradle.kts | 1 - .../device/ScopeAwareContactReader.kt | 21 ++++------------- .../flipcash/app/featureflags/FeatureFlag.kt | 23 ------------------- 4 files changed, 9 insertions(+), 54 deletions(-) diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt index 96d2515c40..489a50c9e3 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt @@ -33,8 +33,6 @@ import com.flipcash.app.core.LocalUserManager import com.flipcash.app.core.extensions.openAsSheet import com.flipcash.app.core.navigation.homeRoute import com.flipcash.app.core.navigation.launchRoute -import com.flipcash.app.featureflags.FeatureFlag -import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.core.onboarding.OnboardingResult import com.flipcash.app.core.onboarding.OnboardingStep import com.flipcash.app.login.internal.LoginAccessKeyViewModel @@ -109,10 +107,7 @@ import kotlin.time.Duration.Companion.milliseconds * Same as (1) but initialStack resumes at the AccessKey, Purchase, or Name step. * ``` * - * ¹ Contact permission is shown only when [FeatureFlag.ContactPickerMode] is off. When - * ContactPickerMode is on, contacts are accessed via the system picker at call site - * (no READ_CONTACTS needed). Already-granted permissions are auto-skipped via - * [PermissionsPhaseFlowHost]. + * ¹ Already-granted permissions are auto-skipped via [PermissionsPhaseFlowHost]. * ² Display-name entry is shown only when no display name is set. It reuses the * UpdateUserProfile subflow, whose `target` replaces the stack with the permissions phase. * ³ A new account (1, and 4 when it resumes account creation) lands on the Wallet tab, so a new @@ -159,21 +154,18 @@ private fun PermissionsPhaseFlowHost( val notificationConfig = PermissionConfigs.notifications() val analytics = rememberAnalytics() - val featureFlags = LocalFeatureFlags.current val userManager = LocalUserManager.current - val contactPickerMode by featureFlags.observe(FeatureFlag.ContactPickerMode).collectAsStateWithLifecycle() val home = onboardingLandingRoute(route) val permissionsSteps = buildList { - if (!route.skipContacts && !contactPickerMode) add(OnboardingStep.ContactPermission) + if (!route.skipContacts) add(OnboardingStep.ContactPermission) add(OnboardingStep.NotificationPermission) } - // Compute resumeAt once per steps-list identity. This recomputes when the flag loads - // (steps changes) but NOT when permissions are granted mid-flow, preventing a stale - // recomposition from triggering a spurious BackedOutOfRoot exit. - val resumeAt = remember(permissionsSteps.map { it::class }, contactPickerMode) { + // Compute resumeAt once per steps-list identity, NOT when permissions are granted mid-flow, + // preventing a stale recomposition from triggering a spurious BackedOutOfRoot exit. + val resumeAt = remember(permissionsSteps.map { it::class }) { val contactsGranted = checker.isGranted(contactConfig.permission) val notificationsGranted = !notificationConfig.requiresRuntimeRequest || checker.isGranted(notificationConfig.permission) diff --git a/apps/flipcash/shared/contacts/build.gradle.kts b/apps/flipcash/shared/contacts/build.gradle.kts index 551b6fcbb7..0414284580 100644 --- a/apps/flipcash/shared/contacts/build.gradle.kts +++ b/apps/flipcash/shared/contacts/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { implementation(project(":apps:flipcash:shared:persistence:db")) implementation(project(":apps:flipcash:shared:persistence:sources")) implementation(project(":apps:flipcash:shared:phone")) - implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":libs:encryption:keys")) implementation(project(":libs:network:connectivity:public")) implementation(libs.androidx.datastore) diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt index 2071464c27..32d26d9f5f 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt @@ -3,8 +3,6 @@ package com.flipcash.app.contacts.device import com.flipcash.app.contacts.device.internal.FullAccessContactReader import com.flipcash.app.contacts.device.internal.PickerContactReader import com.flipcash.app.core.contacts.DeviceContact -import com.flipcash.app.featureflags.FeatureFlag -import com.flipcash.app.featureflags.FeatureFlagController import javax.inject.Inject import javax.inject.Singleton @@ -12,14 +10,12 @@ import javax.inject.Singleton class ScopeAwareContactReader @Inject constructor( private val fullAccess: FullAccessContactReader, private val picker: PickerContactReader, - private val featureFlags: FeatureFlagController, ) : DeviceContactReader { override suspend fun readAll(): Result> { - val reader = activeReader() - val result = reader.readAll() + val result = fullAccess.readAll() // If full-access failed (no permission) but the picker has contacts, use those. - if (result.isFailure && reader === fullAccess) { + if (result.isFailure) { val pickerResult = picker.readAll() if (pickerResult.isSuccess && pickerResult.getOrThrow().isNotEmpty()) { return pickerResult @@ -40,15 +36,6 @@ class ScopeAwareContactReader @Inject constructor( picker.clearPickedContacts() } - /** - * Returns true if READ_CONTACTS was previously used but is now denied. - * Always false in picker mode — picker never holds READ_CONTACTS. - */ - suspend fun isPermissionRevoked(): Boolean { - if (featureFlags.observe(FeatureFlag.ContactPickerMode).value) return false - return fullAccess.readAll().isFailure - } - - private fun activeReader(): DeviceContactReader = - if (featureFlags.observe(FeatureFlag.ContactPickerMode).value) picker else fullAccess + /** Returns true if READ_CONTACTS was previously used but is now denied. */ + suspend fun isPermissionRevoked(): Boolean = fullAccess.readAll().isFailure } diff --git a/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt b/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt index a6c13c9dac..00e330d8dc 100644 --- a/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt +++ b/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt @@ -1,6 +1,5 @@ package com.flipcash.app.featureflags -import android.os.Build import com.flipcash.app.featureflags.model.BackgroundResetTimeout import com.flipcash.app.ksp.annotations.FeatureFlagMarker @@ -80,15 +79,6 @@ sealed interface FeatureFlag { .map { FlagOption(it.name, it.label, isDisabled = it.duration == null) } } - @FeatureFlagMarker - data object ContactPickerMode : FeatureFlag { - override val key: String = "contact_picker_mode" - override val default: Boolean = false - override val launched: Boolean = false - override val visible: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN - override val persistLogOut: Boolean = true - } - @FeatureFlagMarker data object ShowNetworkState: FeatureFlag { override val key: String = "show_network_state_enabled" @@ -98,15 +88,6 @@ sealed interface FeatureFlag { override val persistLogOut: Boolean = false } - @FeatureFlagMarker - data object FrostedTipCard: FeatureFlag { - override val key: String = "frosted_tip_card_enabled" - override val default: Boolean = false - override val launched: Boolean = false - override val visible: Boolean = true - override val persistLogOut: Boolean = false - } - companion object { val entries: List> get() = FeatureFlagEntries.entries @@ -125,9 +106,7 @@ val FeatureFlag<*>.title: String FeatureFlag.CoinbaseOnRampSandbox -> "Coinbase Onramp Sandbox" FeatureFlag.BillTextures -> "Bill Textures" FeatureFlag.BackgroundReset -> "Background Reset" - FeatureFlag.ContactPickerMode -> "Contact Picker Mode" FeatureFlag.ShowNetworkState -> "Network Offline Indicator" - FeatureFlag.FrostedTipCard -> "Frosted Tip Card" } val FeatureFlag<*>.message: String @@ -137,7 +116,5 @@ val FeatureFlag<*>.message: String FeatureFlag.CoinbaseOnRampSandbox -> "When enabled, Coinbase onramp purchases will use the sandbox environment for testing" FeatureFlag.BillTextures -> "When enabled, you'll gain the ability to select textures for bills during currency creation" FeatureFlag.BackgroundReset -> "Automatically returns the app to the camera screen after a period of inactivity with the app in the background" - FeatureFlag.ContactPickerMode -> "When enabled, contacts will be accessed via the system contact picker instead of requesting full READ_CONTACTS permission" FeatureFlag.ShowNetworkState -> "When enabled, you'll gain the ability to see the network state on the Scanner when offline" - FeatureFlag.FrostedTipCard -> "When enabled, the tip card in the scanner renders as frosted glass over a blurred snapshot of the camera instead of a solid card" }