Skip to content

Commit 2329bd7

Browse files
committed
feat(featureflags): add typed FeatureFlag<T> and background reset behavior
Make FeatureFlag generic (FeatureFlag<T: Any>) so option-based flags carry their typed default directly (e.g. BackgroundResetTimeout.FiveMinutes) instead of a raw string. Boolean flags use FeatureFlag<Boolean> unchanged. Add FlagOption.isDisabled for generic disabled-option detection, replacing hardcoded never string checks in the controller. Add defaultEnabled and defaultOption as computed properties derived from the typed default. Add BackgroundResetEffect composable that records a timestamp on ON_STOP and dismisses all sheets via popUntil when the configured timeout elapses on ON_RESUME. Only triggers on true backgrounding, not ON_PAUSE. Defers the reset when a deeplink is pending — waits for it to be consumed and only pops sheets if the deeplink did not result in any routing. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 6acdbe7 commit 2329bd7

12 files changed

Lines changed: 287 additions & 74 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import androidx.compose.runtime.CompositionLocalProvider
1515
import androidx.compose.runtime.LaunchedEffect
1616
import androidx.compose.runtime.collectAsState
1717
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.mutableLongStateOf
1819
import androidx.compose.runtime.mutableStateOf
1920
import androidx.compose.runtime.remember
2021
import androidx.compose.runtime.setValue
22+
import androidx.compose.runtime.snapshotFlow
2123
import androidx.compose.ui.Modifier
2224
import androidx.compose.ui.res.stringResource
2325
import androidx.compose.ui.semantics.semantics
@@ -32,45 +34,50 @@ import androidx.navigation3.scene.SinglePaneSceneStrategy
3234
import com.flipcash.app.analytics.rememberAnalytics
3335
import com.flipcash.app.android.BuildConfig
3436
import com.flipcash.app.bill.customization.BillPlaygroundScaffold
35-
import com.flipcash.app.core.LocalUserManager
3637
import com.flipcash.app.core.AppRoute
37-
import com.flipcash.app.core.verification.email.LocalEmailCodeChannel
38+
import com.flipcash.app.core.LocalUserManager
39+
import com.flipcash.app.core.extensions.navigateTo
3840
import com.flipcash.app.core.navigation.DeeplinkAction
41+
import com.flipcash.app.core.verification.email.LocalEmailCodeChannel
42+
import com.flipcash.app.featureflags.FeatureFlag
43+
import com.flipcash.app.featureflags.LocalFeatureFlags
44+
import com.flipcash.app.featureflags.model.BackgroundResetTimeout
3945
import com.flipcash.app.internal.ui.navigation.appEntryProvider
4046
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
4147
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
4248
import com.flipcash.app.onramp.CoinbaseOnRampHandler
4349
import com.flipcash.app.onramp.ExternalWalletOnRampHandler
4450
import com.flipcash.app.onramp.LocalExternalWalletOnRampController
45-
import com.flipcash.app.onramp.LocalCoinbaseOnRampController
4651
import com.flipcash.app.router.LocalRouter
4752
import com.flipcash.app.session.LocalSessionController
4853
import com.flipcash.app.theme.FlipcashTheme
4954
import com.flipcash.features.shareapp.R
5055
import com.flipcash.services.user.AuthState
56+
import com.getcode.animation.LocalSharedTransitionScope
5157
import com.getcode.libs.biometrics.BiometricsError
5258
import com.getcode.libs.qr.rememberQrBitmapPainter
5359
import com.getcode.navigation.AppNavHost
60+
import com.getcode.navigation.Sheet
61+
import com.getcode.navigation.core.CodeNavigator
5462
import com.getcode.navigation.core.LocalCodeNavigator
5563
import com.getcode.navigation.core.rememberCodeNavigator
5664
import com.getcode.navigation.extensions.getActivityScopedViewModel
5765
import com.getcode.navigation.results.rememberNavResultStateRegistry
5866
import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy
67+
import com.getcode.navigation.scrim.LocalScrimController
68+
import com.getcode.navigation.scrim.ScrimController
69+
import com.getcode.navigation.scrim.ScrimOverlay
5970
import com.getcode.theme.CodeTheme
6071
import com.getcode.ui.biometrics.LocalBiometricsState
6172
import com.getcode.ui.biometrics.rememberBiometricsState
6273
import com.getcode.ui.components.OnLifecycleEvent
6374
import com.getcode.ui.components.bars.rememberBarManager
6475
import com.getcode.ui.core.RestrictionType
65-
import com.flipcash.app.core.extensions.navigateTo
66-
import com.getcode.animation.LocalSharedTransitionScope
67-
import com.getcode.navigation.scrim.LocalScrimController
68-
import com.getcode.navigation.scrim.ScrimController
69-
import com.getcode.navigation.scrim.ScrimOverlay
7076
import dev.bmcreations.tipkit.TipScaffold
7177
import dev.bmcreations.tipkit.engines.TipsEngine
7278
import dev.theolm.rinku.DeepLink
7379
import dev.theolm.rinku.compose.ext.DeepLinkListener
80+
import kotlinx.coroutines.flow.first
7481

7582
@Composable
7683
internal fun App(
@@ -96,6 +103,7 @@ internal fun App(
96103
}
97104

98105
var deepLink by remember { mutableStateOf<DeepLink?>(null) }
106+
var deeplinkHandled by remember { mutableStateOf(false) }
99107
val userManager = LocalUserManager.current!!
100108
DeepLinkListener {
101109
analytics.deeplinkOpened(it.data)
@@ -239,7 +247,9 @@ internal fun App(
239247
return@LaunchedEffect
240248
}
241249

242-
when (val action = router.dispatch(link)) {
250+
val action = router.dispatch(link)
251+
deeplinkHandled = action != DeeplinkAction.None
252+
when (action) {
243253
is DeeplinkAction.Navigate -> {
244254
// If a verification code targets a screen already open,
245255
// deliver via side-channel and skip navigation.
@@ -323,6 +333,13 @@ internal fun App(
323333
else -> Unit
324334
}
325335
}
336+
337+
BackgroundResetEffect(
338+
navigator = codeNavigator,
339+
deepLink = { deepLink },
340+
deeplinkHandled = { deeplinkHandled },
341+
onReset = { deeplinkHandled = false },
342+
)
326343
}
327344
}
328345
}
@@ -331,3 +348,51 @@ internal fun App(
331348
}
332349
}
333350

351+
@Composable
352+
private fun BackgroundResetEffect(
353+
navigator: CodeNavigator,
354+
deepLink: () -> DeepLink?,
355+
deeplinkHandled: () -> Boolean,
356+
onReset: () -> Unit,
357+
) {
358+
val featureFlags = LocalFeatureFlags.current
359+
val option by featureFlags.getOption(FeatureFlag.BackgroundReset)
360+
.collectAsStateWithLifecycle()
361+
362+
var pendingReset by remember { mutableStateOf(false) }
363+
var backgroundedAt by remember { mutableLongStateOf(0L) }
364+
365+
OnLifecycleEvent { _, event ->
366+
when (event) {
367+
Lifecycle.Event.ON_STOP -> {
368+
backgroundedAt = System.currentTimeMillis()
369+
}
370+
Lifecycle.Event.ON_RESUME -> {
371+
val timeout = runCatching { BackgroundResetTimeout.valueOf(option) }
372+
.getOrNull()
373+
?.duration
374+
375+
if (timeout != null && backgroundedAt > 0L) {
376+
val elapsed = System.currentTimeMillis() - backgroundedAt
377+
if (elapsed >= timeout.inWholeMilliseconds) {
378+
pendingReset = true
379+
}
380+
}
381+
backgroundedAt = 0L
382+
}
383+
else -> Unit
384+
}
385+
}
386+
387+
LaunchedEffect(pendingReset) {
388+
if (!pendingReset) return@LaunchedEffect
389+
// Wait for any pending deeplink to be consumed before deciding
390+
snapshotFlow { deepLink() }.first { it == null }
391+
if (!deeplinkHandled()) {
392+
navigator.popUntil { it !is Sheet }
393+
}
394+
onReset()
395+
pendingReset = false
396+
}
397+
}
398+

apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/components/BillPlayground.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ internal fun Modifier.presenceBorder(
217217
)
218218

219219
private object PreviewFeatureFlagController : FeatureFlagController by NoOpFeatureFlagController {
220-
override fun observe(flag: FeatureFlag): StateFlow<Boolean> = MutableStateFlow(true)
220+
override fun observe(flag: FeatureFlag<*>): StateFlow<Boolean> = MutableStateFlow(true)
221221
}
222222

223223
@Composable

apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
2525
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2626
import com.flipcash.app.core.AppRoute
2727
import com.flipcash.app.core.extensions.navigateTo
28+
import com.flipcash.app.featureflags.FlagOption
2829
import com.flipcash.app.featureflags.LocalFeatureFlags
2930
import com.flipcash.app.featureflags.message
3031
import com.flipcash.app.featureflags.title
@@ -35,6 +36,7 @@ import com.getcode.ui.components.ListItem
3536
import com.getcode.ui.components.SettingsSwitchRow
3637
import com.getcode.ui.components.text.SectionHeader
3738
import com.getcode.ui.core.verticalScrollStateGradient
39+
import com.getcode.ui.theme.CodeSegmentedControl
3840
import com.getcode.ui.utils.sheetResignmentBehavior
3941

4042
@Composable
@@ -59,12 +61,24 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
5961
SectionHeader(stringResource(R.string.title_settingsSectionFeatures))
6062
}
6163
items(betaFlags, key = { it.flag.key }) { feature ->
62-
SettingsSwitchRow(
63-
title = feature.flag.title,
64-
subtitle = feature.flag.message,
65-
checked = feature.enabled
66-
) {
67-
betaFlagsController.set(feature.flag, !feature.enabled)
64+
if (feature.flag.isOptionFlag) {
65+
SettingsOptionRow(
66+
title = feature.flag.title,
67+
subtitle = feature.flag.message,
68+
options = feature.flag.options,
69+
selectedOption = feature.selectedOption ?: feature.flag.defaultOption,
70+
onOptionSelected = { optionKey ->
71+
betaFlagsController.setOption(feature.flag, optionKey)
72+
},
73+
)
74+
} else {
75+
SettingsSwitchRow(
76+
title = feature.flag.title,
77+
subtitle = feature.flag.message,
78+
checked = feature.enabled
79+
) {
80+
betaFlagsController.set(feature.flag, !feature.enabled)
81+
}
6882
}
6983

7084
HorizontalDivider(
@@ -133,4 +147,46 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
133147
}
134148
}
135149
}
150+
}
151+
152+
@Composable
153+
private fun SettingsOptionRow(
154+
title: String,
155+
subtitle: String?,
156+
options: List<FlagOption>,
157+
selectedOption: String,
158+
onOptionSelected: (String) -> Unit,
159+
) {
160+
Column(
161+
modifier = Modifier
162+
.fillMaxWidth()
163+
.padding(horizontal = CodeTheme.dimens.grid.x3)
164+
.padding(vertical = CodeTheme.dimens.grid.x3),
165+
) {
166+
Text(
167+
text = title,
168+
color = CodeTheme.colors.textMain,
169+
style = CodeTheme.typography.textMedium,
170+
)
171+
if (!subtitle.isNullOrEmpty()) {
172+
Text(
173+
text = subtitle,
174+
style = CodeTheme.typography.textSmall,
175+
color = CodeTheme.colors.textSecondary,
176+
)
177+
}
178+
CodeSegmentedControl(
179+
options = options,
180+
selected = options.find { it.key == selectedOption },
181+
modifier = Modifier
182+
.fillMaxWidth()
183+
.padding(top = CodeTheme.dimens.grid.x2),
184+
mapper = { option ->
185+
Text(text = option.label)
186+
},
187+
onSelectionChanged = { option ->
188+
onOptionSelected(option.key)
189+
},
190+
)
191+
}
136192
}

apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuItems.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ internal data object SwitchAccount : StaffMenuItem<MenuScreenViewModel.Event>()
6363
override val name: String
6464
@Composable get() = stringResource(R.string.title_switchAccounts)
6565
override val action: MenuScreenViewModel.Event = MenuScreenViewModel.Event.OnSwitchAccountsClicked
66-
override val featureFlag: FeatureFlag = FeatureFlag.CredentialManager
66+
override val featureFlag: FeatureFlag<*> = FeatureFlag.CredentialManager
6767
}
6868

6969
internal data object Labs : StaffMenuItem<MenuScreenViewModel.Event>() {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.app.featureflags
22

33
data class BetaFeature(
4-
val flag: FeatureFlag,
4+
val flag: FeatureFlag<*>,
55
val enabled: Boolean,
6+
val selectedOption: String? = null,
67
)

0 commit comments

Comments
 (0)