Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion apps/flipcash/shared/contacts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@ 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

@Singleton
class ScopeAwareContactReader @Inject constructor(
private val fullAccess: FullAccessContactReader,
private val picker: PickerContactReader,
private val featureFlags: FeatureFlagController,
) : DeviceContactReader {

override suspend fun readAll(): Result<Map<String, DeviceContact>> {
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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -80,15 +79,6 @@ sealed interface FeatureFlag<T: Any> {
.map { FlagOption(it.name, it.label, isDisabled = it.duration == null) }
}

@FeatureFlagMarker
data object ContactPickerMode : FeatureFlag<Boolean> {
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<Boolean> {
override val key: String = "show_network_state_enabled"
Expand All @@ -98,15 +88,6 @@ sealed interface FeatureFlag<T: Any> {
override val persistLogOut: Boolean = false
}

@FeatureFlagMarker
data object FrostedTipCard: FeatureFlag<Boolean> {
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<FeatureFlag<*>>
get() = FeatureFlagEntries.entries
Expand All @@ -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
Expand All @@ -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"
}
Loading