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 @@ -7,6 +7,8 @@ import androidx.core.app.NotificationManagerCompat
import com.flipcash.app.android.BuildConfig
import com.flipcash.app.core.android.VersionInfo
import com.flipcash.app.core.annotations.AccountType
import com.flipcash.app.core.toast.SystemToastController
import com.flipcash.app.internal.toast.AndroidSystemToastController
import com.getcode.util.resources.AndroidContentReader
import com.getcode.util.resources.AndroidResources
import com.getcode.util.resources.AndroidSettingsHelper
Expand Down Expand Up @@ -66,4 +68,10 @@ object AppModule {
fun providesBiometricsManager(
@ApplicationContext context: Context
): BiometricManager = BiometricManager.from(context)

@Provides
@Singleton
fun providesSystemToastController(
@ApplicationContext context: Context
): SystemToastController = AndroidSystemToastController(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.flipcash.app.internal.toast

import android.content.Context
import android.widget.Toast
import com.flipcash.app.core.toast.SystemToastController

internal class AndroidSystemToastController(
private val context: Context,
) : SystemToastController {
private var activeToast: Toast? = null

private fun show(toast: Toast, replacePrevious: Boolean) {
if (replacePrevious) {
activeToast?.cancel()
}
activeToast = toast
toast.show()
}

override fun showToast(message: String, replacePrevious: Boolean) {
show(Toast.makeText(context, message, Toast.LENGTH_LONG), replacePrevious)
}

override fun showToast(messageRes: Int, vararg args: Any, replacePrevious: Boolean) {
show(Toast.makeText(context, context.getString(messageRes, *args), Toast.LENGTH_SHORT), replacePrevious)
}

override fun showQuantityToast(pluralRes: Int, quantity: Int, vararg args: Any, replacePrevious: Boolean) {
show(Toast.makeText(context, context.resources.getQuantityString(pluralRes, quantity, *args), Toast.LENGTH_SHORT), replacePrevious)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fun appEntryProvider(

// Menu
annotatedEntry<AppRoute.Menu.AppSettings> { AppSettingsScreen() }
annotatedEntry<AppRoute.Menu.Lab> { LabsScreen() }
annotatedEntry<AppRoute.Menu.Lab> { key -> LabsScreen(onboarding = key.onboarding) }
annotatedEntry<AppRoute.Menu.NavBarSettings> { NavBarSettingsScreen() }
annotatedEntry<AppRoute.Menu.UserProfile> { UserProfileScreen() }
annotatedEntry<AppRoute.Menu.MyAccount> { MyAccountScreen() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ sealed interface AppRoute : NavKey, Parcelable {
@Serializable
data object UserProfile : Menu
@Serializable
data object Lab : Menu
data class Lab(val onboarding: Boolean = false) : Menu
@Serializable
data object NavBarSettings : Menu, com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flipcash.app.core.toast

import androidx.annotation.PluralsRes
import androidx.annotation.StringRes

interface SystemToastController {
fun showToast(message: String, replacePrevious: Boolean = false)
fun showToast(@StringRes messageRes: Int, vararg args: Any, replacePrevious: Boolean = false)
fun showQuantityToast(@PluralsRes pluralRes: Int, quantity: Int, vararg args: Any, replacePrevious: Boolean = false)
}
10 changes: 10 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@
<string name="title_labsAreEmpty">Nothing Cooking in the Lab Right Now</string>
<string name="subtitle_labsAreEmpty">Check back in the next app update.</string>

<string name="title_betaOverride">Beta Overrides</string>
<string name="subtitle_betaOverride">All flags are visible. Toggle off to reset flags to defaults.</string>
<string name="toast_betaOverrideDisabled">Flags reset to defaults. Changes take effect on next visit.</string>
<string name="toast_betaOverrideEnabled">You are now a developer!</string>
<string name="toast_betaOverrideAlready">No need, you are already a developer</string>
<plurals name="toast_betaOverrideCountdown">
<item quantity="one">You are now %1$d step away from being a developer</item>
<item quantity="other">You are now %1$d steps away from being a developer</item>
</plurals>

<string name="prompt_title_deleteAccount">Permanently Delete Account?</string>
<string name="prompt_description_deleteAccount">This will permanently delete your Flipcash account</string>
<string name="action_deleteAccount">Delete Account</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ internal data object BetaFlags : FullMenuItem<AdvancedFeaturesScreenViewModel.Ev
override val name: String
@Composable get() = stringResource(R.string.title_betaFlags)
override val action: AdvancedFeaturesScreenViewModel.Event =
AdvancedFeaturesScreenViewModel.Event.OpenScreen(AppRoute.Menu.Lab)
AdvancedFeaturesScreenViewModel.Event.OpenScreen(AppRoute.Menu.Lab())
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import com.getcode.ui.components.AppBarDefaults
import com.getcode.ui.components.AppBarWithTitle

@Composable
fun LabsScreen() {
fun LabsScreen(onboarding: Boolean = false) {
val navigator = LocalCodeNavigator.current
val isSheetRoot = remember(navigator) { navigator.backStack.size <= 1 }
Column(
Expand Down Expand Up @@ -44,6 +44,6 @@ fun LabsScreen() {

val viewModel = getActivityScopedViewModel<LabsScreenViewModel>()

LabsScreenContent(viewModel)
LabsScreenContent(viewModel, onboarding)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContactMail
import androidx.compose.material.icons.filled.Token
import androidx.compose.material.Surface
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
Expand All @@ -41,16 +45,24 @@ import com.getcode.ui.theme.CodeSegmentedControl
import com.getcode.ui.utils.sheetResignmentBehavior

@Composable
internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
internal fun LabsScreenContent(viewModel: LabsScreenViewModel, onboarding: Boolean = false) {
val betaFlagsController = LocalFeatureFlags.current
val allFlags by betaFlagsController.observe().collectAsStateWithLifecycle()
val betaOverride by viewModel.betaOverride.collectAsStateWithLifecycle()
val navigator = LocalCodeNavigator.current
val isStaff by viewModel.isStaff.collectAsStateWithLifecycle()

val betaFlags = remember(allFlags, betaOverride) {
if (betaOverride) allFlags
else allFlags.filter { it.flag.minTrack == FeatureTrack.Production }
// Keep showing all flags even after toggling off override, until leaving the screen
var showAllFlags by remember { mutableStateOf(betaOverride) }
LaunchedEffect(betaOverride) {
if (betaOverride) showAllFlags = true
}

val betaFlags = remember(allFlags, showAllFlags, onboarding) {
if (showAllFlags) allFlags
else allFlags.filter {
it.flag.minTrack == FeatureTrack.Production || (onboarding && it.flag.onboarding)
}
}

val state = rememberLazyListState()
Expand All @@ -64,7 +76,28 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
.sheetResignmentBehavior(state),
contentPadding = PaddingValues(bottom = CodeTheme.dimens.grid.x3),
) {
item(contentType = "section_header") {
if (showAllFlags) {
item(contentType = "override_toggle") {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = CodeTheme.dimens.inset)
.padding(top = CodeTheme.dimens.grid.x2),
shape = CodeTheme.shapes.medium,
color = CodeTheme.colors.surfaceVariant,
) {
SettingsSwitchRow(
title = stringResource(R.string.title_betaOverride),
subtitle = stringResource(R.string.subtitle_betaOverride),
checked = betaOverride,
) {
viewModel.disableBetaFeatures()
}
}
}
}

item(contentType = "section_header") {
SectionHeader(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
title = stringResource(R.string.title_settingsSectionFeatures)
Expand Down Expand Up @@ -127,7 +160,7 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
}
}

if (betaOverride) {
if (showAllFlags) {
item(contentType = "section_header") {
SectionHeader(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
Expand All @@ -144,7 +177,7 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
}
}

if (betaOverride && isStaff) {
if (showAllFlags && isStaff) {
item(contentType = "section_header") {
SectionHeader(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.flipcash.app.lab.internal

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.flipcash.app.core.toast.SystemToastController
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.core.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
Expand All @@ -13,11 +15,17 @@ import javax.inject.Inject
@HiltViewModel
class LabsScreenViewModel @Inject constructor(
userFlags: UserFlagsCoordinator,
featureFlagController: FeatureFlagController,
private val featureFlagController: FeatureFlagController,
private val toastController: SystemToastController,
) : ViewModel() {

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

val betaOverride = featureFlagController.observeOverride()

fun disableBetaFeatures() {
featureFlagController.disableBetaFeatures()
toastController.showToast(R.string.toast_betaOverrideDisabled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ private fun LoginStepContent(seed: String?) {
login = { flowNavigator.navigateTo(OnboardingStep.SeedInput) },
isLabsOpen = state.betaOptionsVisible,
onLogoTapped = { vm.dispatchEvent(LoginViewModel.Event.OnLogoTapped) },
openBetaFlags = { outerNavigator.openAsSheet(AppRoute.Menu.Lab) },
openBetaFlags = { outerNavigator.openAsSheet(AppRoute.Menu.Lab(onboarding = true)) },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.flipcash.app.core.android.VersionInfo
import com.flipcash.app.core.extensions.onResult
import com.flipcash.app.featureflags.BetaFeature
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.core.toast.SystemToastController
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.menu.MenuItem
import com.flipcash.app.payments.PurchaseMethodController
Expand All @@ -19,7 +20,6 @@ import com.flipcash.services.user.AuthState
import com.flipcash.services.user.UserManager
import com.getcode.manager.BottomBarManager
import com.getcode.opencode.managers.MnemonicManager
import com.getcode.util.resources.ResourceHelper
import com.flipcash.libs.coroutines.DispatcherProvider
import com.getcode.manager.BottomBarAction
import com.getcode.view.BaseViewModel
Expand Down Expand Up @@ -53,6 +53,7 @@ internal class MenuScreenViewModel @Inject constructor(
versionInfo: VersionInfo,
mnemonicManager: MnemonicManager,
featureFlags: FeatureFlagController,
private val toastController: SystemToastController,
dispatchers: DispatcherProvider,
releaseStageProvider: ReleaseStageProvider,
purchaseMethodController: PurchaseMethodController,
Expand Down Expand Up @@ -120,10 +121,24 @@ internal class MenuScreenViewModel @Inject constructor(

eventFlow
.filterIsInstance<Event.OnVersionInfoClicked>()
.map { stateFlow.value.logoTapCount }
.filter { it > TAP_THRESHOLD }
.filterNot { stateFlow.value.unlockedBetaFeaturesManually }
.onEach { featureFlags.enableBetaFeatures() }
.onEach {
if (stateFlow.value.unlockedBetaFeaturesManually) {
if (stateFlow.value.logoTapCount - TAP_THRESHOLD > COUNTDOWN_START) {
toastController.showToast(R.string.toast_betaOverrideAlready, replacePrevious = true)
}
return@onEach
}
val remaining = TAP_THRESHOLD - stateFlow.value.logoTapCount + 1
when {
remaining <= 0 -> {
featureFlags.enableBetaFeatures()
toastController.showToast(R.string.toast_betaOverrideEnabled, replacePrevious = true)
}
remaining <= COUNTDOWN_START -> {
toastController.showQuantityToast(R.plurals.toast_betaOverrideCountdown, remaining, remaining, replacePrevious = true)
}
}
}
.launchIn(viewModelScope)

@OptIn(FlowPreview::class)
Expand Down Expand Up @@ -165,6 +180,7 @@ internal class MenuScreenViewModel @Inject constructor(

internal companion object {
private const val TAP_THRESHOLD = 6
private const val COUNTDOWN_START = 3

private fun buildItemList(
isStaff: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed interface FeatureFlag<T: Any> {
val visible: Boolean
val persistLogOut: Boolean
val minTrack: FeatureTrack get() = FeatureTrack.Internal
val onboarding: Boolean get() = false
val options: List<FlagOption> get() = emptyList()
val defaultOption: String
get() = if (default is Enum<*>) (default as Enum<*>).name else ""
Expand All @@ -40,6 +41,7 @@ sealed interface FeatureFlag<T: Any> {
override val launched: Boolean = false
override val visible: Boolean = true
override val persistLogOut: Boolean = true
override val onboarding: Boolean = true
}

@FeatureFlagMarker
Expand Down Expand Up @@ -205,6 +207,7 @@ sealed interface FeatureFlag<T: Any> {
override val launched: Boolean = false
override val visible: Boolean = true
override val persistLogOut: Boolean = true
override val onboarding: Boolean = true
}

@FeatureFlagMarker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.StateFlow

interface FeatureFlagController {
fun enableBetaFeatures()
fun disableBetaFeatures()
fun observeOverride(): StateFlow<Boolean>
fun set(flag: FeatureFlag<*>, value: Boolean)
suspend fun get(flag: FeatureFlag<*>): Boolean
Expand All @@ -19,6 +20,7 @@ interface FeatureFlagController {

object NoOpFeatureFlagController : FeatureFlagController {
override fun enableBetaFeatures() = Unit
override fun disableBetaFeatures() = Unit
override fun observeOverride(): StateFlow<Boolean> = MutableStateFlow(false)
override fun set(flag: FeatureFlag<*>, value: Boolean) = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ internal class InternalFeatureFlagController @Inject constructor(
}
}

override fun disableBetaFeatures() {
dataScope.launch(Dispatchers.IO) {
betaFlags.edit { prefs ->
prefs[betaOverrideKey] = false
FeatureFlag.availableEntries.forEach { flag ->
prefs.remove(flag.booleanPreferenceKey)
if (flag.isOptionFlag) {
prefs.remove(flag.optionPreferenceKey)
}
}
}
}
}

override fun observeOverride(): StateFlow<Boolean> =
betaFlags.data.map { prefs -> prefs[betaOverrideKey] ?: false }
.stateIn(dataScope, SharingStarted.Eagerly, false)
Expand Down
Loading