Skip to content

Commit 048f0e6

Browse files
authored
feat(flags): add beta override toggle with developer easter egg (#905)
* feat(flags): add beta override toggle with developer easter egg Add a toggle switch on the Labs screen to disable beta overrides, resetting all flags to defaults. Flags remain visible until leaving the screen. Introduce SystemToastController for VM-driven toasts with opt-in previous-toast cancellation. Signed-off-by: Brandon McAnsh <git@bmcreations.dev> * chore: allow onboarding beta flags to be visible from login Signed-off-by: Brandon McAnsh <git@bmcreations.dev> --------- Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 11ad9ff commit 048f0e6

15 files changed

Lines changed: 154 additions & 19 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/inject/AppModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import androidx.core.app.NotificationManagerCompat
77
import com.flipcash.app.android.BuildConfig
88
import com.flipcash.app.core.android.VersionInfo
99
import com.flipcash.app.core.annotations.AccountType
10+
import com.flipcash.app.core.toast.SystemToastController
11+
import com.flipcash.app.internal.toast.AndroidSystemToastController
1012
import com.getcode.util.resources.AndroidContentReader
1113
import com.getcode.util.resources.AndroidResources
1214
import com.getcode.util.resources.AndroidSettingsHelper
@@ -66,4 +68,10 @@ object AppModule {
6668
fun providesBiometricsManager(
6769
@ApplicationContext context: Context
6870
): BiometricManager = BiometricManager.from(context)
71+
72+
@Provides
73+
@Singleton
74+
fun providesSystemToastController(
75+
@ApplicationContext context: Context
76+
): SystemToastController = AndroidSystemToastController(context)
6977
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.flipcash.app.internal.toast
2+
3+
import android.content.Context
4+
import android.widget.Toast
5+
import com.flipcash.app.core.toast.SystemToastController
6+
7+
internal class AndroidSystemToastController(
8+
private val context: Context,
9+
) : SystemToastController {
10+
private var activeToast: Toast? = null
11+
12+
private fun show(toast: Toast, replacePrevious: Boolean) {
13+
if (replacePrevious) {
14+
activeToast?.cancel()
15+
}
16+
activeToast = toast
17+
toast.show()
18+
}
19+
20+
override fun showToast(message: String, replacePrevious: Boolean) {
21+
show(Toast.makeText(context, message, Toast.LENGTH_LONG), replacePrevious)
22+
}
23+
24+
override fun showToast(messageRes: Int, vararg args: Any, replacePrevious: Boolean) {
25+
show(Toast.makeText(context, context.getString(messageRes, *args), Toast.LENGTH_SHORT), replacePrevious)
26+
}
27+
28+
override fun showQuantityToast(pluralRes: Int, quantity: Int, vararg args: Any, replacePrevious: Boolean) {
29+
show(Toast.makeText(context, context.resources.getQuantityString(pluralRes, quantity, *args), Toast.LENGTH_SHORT), replacePrevious)
30+
}
31+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ fun appEntryProvider(
122122

123123
// Menu
124124
annotatedEntry<AppRoute.Menu.AppSettings> { AppSettingsScreen() }
125-
annotatedEntry<AppRoute.Menu.Lab> { LabsScreen() }
125+
annotatedEntry<AppRoute.Menu.Lab> { key -> LabsScreen(onboarding = key.onboarding) }
126126
annotatedEntry<AppRoute.Menu.NavBarSettings> { NavBarSettingsScreen() }
127127
annotatedEntry<AppRoute.Menu.UserProfile> { UserProfileScreen() }
128128
annotatedEntry<AppRoute.Menu.MyAccount> { MyAccountScreen() }

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ sealed interface AppRoute : NavKey, Parcelable {
233233
@Serializable
234234
data object UserProfile : Menu
235235
@Serializable
236-
data object Lab : Menu
236+
data class Lab(val onboarding: Boolean = false) : Menu
237237
@Serializable
238238
data object NavBarSettings : Menu, com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet
239239
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.flipcash.app.core.toast
2+
3+
import androidx.annotation.PluralsRes
4+
import androidx.annotation.StringRes
5+
6+
interface SystemToastController {
7+
fun showToast(message: String, replacePrevious: Boolean = false)
8+
fun showToast(@StringRes messageRes: Int, vararg args: Any, replacePrevious: Boolean = false)
9+
fun showQuantityToast(@PluralsRes pluralRes: Int, quantity: Int, vararg args: Any, replacePrevious: Boolean = false)
10+
}

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@
131131
<string name="title_labsAreEmpty">Nothing Cooking in the Lab Right Now</string>
132132
<string name="subtitle_labsAreEmpty">Check back in the next app update.</string>
133133

134+
<string name="title_betaOverride">Beta Overrides</string>
135+
<string name="subtitle_betaOverride">All flags are visible. Toggle off to reset flags to defaults.</string>
136+
<string name="toast_betaOverrideDisabled">Flags reset to defaults. Changes take effect on next visit.</string>
137+
<string name="toast_betaOverrideEnabled">You are now a developer!</string>
138+
<string name="toast_betaOverrideAlready">No need, you are already a developer</string>
139+
<plurals name="toast_betaOverrideCountdown">
140+
<item quantity="one">You are now %1$d step away from being a developer</item>
141+
<item quantity="other">You are now %1$d steps away from being a developer</item>
142+
</plurals>
143+
134144
<string name="prompt_title_deleteAccount">Permanently Delete Account?</string>
135145
<string name="prompt_description_deleteAccount">This will permanently delete your Flipcash account</string>
136146
<string name="action_deleteAccount">Delete Account</string>

apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeatureMenuItems.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ internal data object BetaFlags : FullMenuItem<AdvancedFeaturesScreenViewModel.Ev
3636
override val name: String
3737
@Composable get() = stringResource(R.string.title_betaFlags)
3838
override val action: AdvancedFeaturesScreenViewModel.Event =
39-
AdvancedFeaturesScreenViewModel.Event.OpenScreen(AppRoute.Menu.Lab)
39+
AdvancedFeaturesScreenViewModel.Event.OpenScreen(AppRoute.Menu.Lab())
4040
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.getcode.ui.components.AppBarDefaults
1616
import com.getcode.ui.components.AppBarWithTitle
1717

1818
@Composable
19-
fun LabsScreen() {
19+
fun LabsScreen(onboarding: Boolean = false) {
2020
val navigator = LocalCodeNavigator.current
2121
val isSheetRoot = remember(navigator) { navigator.backStack.size <= 1 }
2222
Column(
@@ -44,6 +44,6 @@ fun LabsScreen() {
4444

4545
val viewModel = getActivityScopedViewModel<LabsScreenViewModel>()
4646

47-
LabsScreenContent(viewModel)
47+
LabsScreenContent(viewModel, onboarding)
4848
}
4949
}

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ import androidx.compose.foundation.lazy.rememberLazyListState
1212
import androidx.compose.material.icons.Icons
1313
import androidx.compose.material.icons.filled.ContactMail
1414
import androidx.compose.material.icons.filled.Token
15+
import androidx.compose.material.Surface
1516
import androidx.compose.material3.HorizontalDivider
1617
import androidx.compose.material3.Text
1718
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.LaunchedEffect
1820
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
1922
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
2024
import androidx.compose.ui.Alignment
2125
import androidx.compose.ui.Modifier
2226
import androidx.compose.ui.graphics.vector.rememberVectorPainter
@@ -41,16 +45,24 @@ import com.getcode.ui.theme.CodeSegmentedControl
4145
import com.getcode.ui.utils.sheetResignmentBehavior
4246

4347
@Composable
44-
internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
48+
internal fun LabsScreenContent(viewModel: LabsScreenViewModel, onboarding: Boolean = false) {
4549
val betaFlagsController = LocalFeatureFlags.current
4650
val allFlags by betaFlagsController.observe().collectAsStateWithLifecycle()
4751
val betaOverride by viewModel.betaOverride.collectAsStateWithLifecycle()
4852
val navigator = LocalCodeNavigator.current
4953
val isStaff by viewModel.isStaff.collectAsStateWithLifecycle()
5054

51-
val betaFlags = remember(allFlags, betaOverride) {
52-
if (betaOverride) allFlags
53-
else allFlags.filter { it.flag.minTrack == FeatureTrack.Production }
55+
// Keep showing all flags even after toggling off override, until leaving the screen
56+
var showAllFlags by remember { mutableStateOf(betaOverride) }
57+
LaunchedEffect(betaOverride) {
58+
if (betaOverride) showAllFlags = true
59+
}
60+
61+
val betaFlags = remember(allFlags, showAllFlags, onboarding) {
62+
if (showAllFlags) allFlags
63+
else allFlags.filter {
64+
it.flag.minTrack == FeatureTrack.Production || (onboarding && it.flag.onboarding)
65+
}
5466
}
5567

5668
val state = rememberLazyListState()
@@ -64,7 +76,28 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
6476
.sheetResignmentBehavior(state),
6577
contentPadding = PaddingValues(bottom = CodeTheme.dimens.grid.x3),
6678
) {
67-
item(contentType = "section_header") {
79+
if (showAllFlags) {
80+
item(contentType = "override_toggle") {
81+
Surface(
82+
modifier = Modifier
83+
.fillMaxWidth()
84+
.padding(horizontal = CodeTheme.dimens.inset)
85+
.padding(top = CodeTheme.dimens.grid.x2),
86+
shape = CodeTheme.shapes.medium,
87+
color = CodeTheme.colors.surfaceVariant,
88+
) {
89+
SettingsSwitchRow(
90+
title = stringResource(R.string.title_betaOverride),
91+
subtitle = stringResource(R.string.subtitle_betaOverride),
92+
checked = betaOverride,
93+
) {
94+
viewModel.disableBetaFeatures()
95+
}
96+
}
97+
}
98+
}
99+
100+
item(contentType = "section_header") {
68101
SectionHeader(
69102
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
70103
title = stringResource(R.string.title_settingsSectionFeatures)
@@ -127,7 +160,7 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
127160
}
128161
}
129162

130-
if (betaOverride) {
163+
if (showAllFlags) {
131164
item(contentType = "section_header") {
132165
SectionHeader(
133166
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
@@ -144,7 +177,7 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
144177
}
145178
}
146179

147-
if (betaOverride && isStaff) {
180+
if (showAllFlags && isStaff) {
148181
item(contentType = "section_header") {
149182
SectionHeader(
150183
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package com.flipcash.app.lab.internal
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import com.flipcash.app.core.toast.SystemToastController
56
import com.flipcash.app.featureflags.FeatureFlagController
67
import com.flipcash.app.userflags.UserFlagsCoordinator
8+
import com.flipcash.core.R
79
import dagger.hilt.android.lifecycle.HiltViewModel
810
import kotlinx.coroutines.flow.SharingStarted
911
import kotlinx.coroutines.flow.map
@@ -13,11 +15,17 @@ import javax.inject.Inject
1315
@HiltViewModel
1416
class LabsScreenViewModel @Inject constructor(
1517
userFlags: UserFlagsCoordinator,
16-
featureFlagController: FeatureFlagController,
18+
private val featureFlagController: FeatureFlagController,
19+
private val toastController: SystemToastController,
1720
) : ViewModel() {
1821

1922
val isStaff = userFlags.resolvedFlags.map { it.isStaff.effectiveValue }
2023
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = false)
2124

2225
val betaOverride = featureFlagController.observeOverride()
26+
27+
fun disableBetaFeatures() {
28+
featureFlagController.disableBetaFeatures()
29+
toastController.showToast(R.string.toast_betaOverrideDisabled)
30+
}
2331
}

0 commit comments

Comments
 (0)