diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index aee91a6fa6..2c93fe1a90 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -273,7 +273,6 @@
Add money to send cash
Add money to get started
Add money to create a currency
- Buy your first currency to get started
Add money to send tips
Dismiss
Success
diff --git a/apps/flipcash/features/balance/build.gradle.kts b/apps/flipcash/features/balance/build.gradle.kts
index d249c016e1..c3a9a99b4d 100644
--- a/apps/flipcash/features/balance/build.gradle.kts
+++ b/apps/flipcash/features/balance/build.gradle.kts
@@ -8,6 +8,8 @@ android {
dependencies {
testImplementation(kotlin("test"))
+ testImplementation(libs.bundles.unit.testing)
+ testImplementation(libs.bundles.compose.ui.testing)
implementation(libs.compose.paging)
diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt
index a3efa7b353..9cc3639a1e 100644
--- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt
+++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt
@@ -48,15 +48,13 @@ internal fun BalanceScreen(
val balanceState by viewModel.stateFlow.collectAsStateWithLifecycle()
val tokenState by tokenViewModel.stateFlow.collectAsStateWithLifecycle()
BalanceScreenContent(
- addMoneyUx = balanceState.depositFirstUx,
tokenState = tokenState,
dispatchEvent = viewModel::dispatchEvent
)
}
@Composable
-private fun BalanceScreenContent(
- addMoneyUx: Boolean = false,
+internal fun BalanceScreenContent(
tokenState: SelectTokenViewModel.State,
dispatchEvent: (BalanceViewModel.Event) -> Unit
) {
@@ -109,11 +107,7 @@ private fun BalanceScreenContent(
Text(
modifier = Modifier.fillMaxWidth(0.6f),
- text = if (addMoneyUx) {
- stringResource(R.string.description_noBalanceYetForBalance)
- } else {
- stringResource(R.string.description_noBalanceYetDiscover)
- },
+ text = stringResource(R.string.description_noBalanceYetForBalance),
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
textAlign = TextAlign.Center,
@@ -127,11 +121,7 @@ private fun BalanceScreenContent(
.padding(top = CodeTheme.dimens.grid.x2)
.align(Alignment.CenterHorizontally),
contentPadding = PaddingValues(),
- text = if (addMoneyUx) {
- stringResource(R.string.action_addMoney)
- } else {
- stringResource(R.string.action_discoverCurrencies)
- },
+ text = stringResource(R.string.action_addMoney),
shape = CircleShape,
)
}
@@ -146,22 +136,12 @@ private fun BalanceScreenContent(
.padding(horizontal = CodeTheme.dimens.inset)
.padding(bottom = CodeTheme.dimens.grid.x3)
.navigationBarsPadding(),
- text = if (addMoneyUx) {
- stringResource(R.string.action_addMoney)
- } else {
- stringResource(R.string.action_discoverCurrencies)
- },
+ text = stringResource(R.string.action_addMoney),
buttonState = ButtonState.Filled10,
onClick = {
- if (addMoneyUx) {
- dispatchEvent(
- BalanceViewModel.Event.PresentDepositOptions
- )
- } else {
- dispatchEvent(
- BalanceViewModel.Event.OpenScreen(AppRoute.Token.Discovery)
- )
- }
+ dispatchEvent(
+ BalanceViewModel.Event.PresentDepositOptions
+ )
}
)
}
@@ -196,7 +176,6 @@ private fun Preview_BalanceScreen_Empty() {
) {
Box(modifier = Modifier.background(CodeTheme.colors.background)) {
BalanceScreenContent(
- addMoneyUx = true,
tokenState = SelectTokenViewModel.State(
purpose = TokenPurpose.Balance,
tokens = emptyList()
diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt
index 2a29fe792d..86955c0750 100644
--- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt
+++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt
@@ -4,8 +4,6 @@ import androidx.lifecycle.viewModelScope
import com.flipcash.app.analytics.Analytics
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.core.AppRoute
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.funding.PurchaseMethodController
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
@@ -29,7 +27,6 @@ internal class BalanceViewModel @Inject constructor(
userFlags: UserFlagsCoordinator,
dispatchers: DispatcherProvider,
purchaseMethodController: PurchaseMethodController,
- featureFlags: FeatureFlagController,
analytics: FlipcashAnalyticsService,
) : BaseViewModel(
initialState = State(),
@@ -37,12 +34,10 @@ internal class BalanceViewModel @Inject constructor(
defaultDispatcher = dispatchers.Default,
) {
data class State(
- val depositFirstUx: Boolean = false,
val preferredOnRampProvider: OnRampProvider.Defined? = null,
)
sealed interface Event {
- data class DepositFirstUxEnabled(val enabled: Boolean): Event
data class OnPreferredOnRampProviderChanged(val provider: OnRampProvider.Defined?) : Event
data object OpenCurrencySelection : Event
@@ -52,10 +47,6 @@ internal class BalanceViewModel @Inject constructor(
}
init {
- featureFlags.observe(FeatureFlag.AddMoneyUX)
- .onEach { dispatchEvent(Event.DepositFirstUxEnabled(it)) }
- .launchIn(viewModelScope)
-
userManager.state
.filter { it.authState is AuthState.Ready }
.flatMapLatest { userFlags.resolvedFlags }
@@ -65,12 +56,7 @@ internal class BalanceViewModel @Inject constructor(
eventFlow
.filterIsInstance()
- .map { stateFlow.value.depositFirstUx }
- .mapNotNull { depositFirstUx ->
- if (!depositFirstUx) {
- dispatchEvent(Event.OpenScreen(AppRoute.Token.Discovery))
- return@mapNotNull null
- }
+ .mapNotNull {
analytics.addMoneyOpened(Analytics.AddMoneySource.Balance)
purchaseMethodController.presentDepositOptions(popToRoot = true) }
.onEach { route -> dispatchEvent(Event.OpenScreen(route)) }
@@ -86,7 +72,6 @@ internal class BalanceViewModel @Inject constructor(
}
Event.PresentDepositOptions -> { state -> state }
is Event.OpenScreen -> { state -> state }
- is Event.DepositFirstUxEnabled -> { state -> state.copy(depositFirstUx = event.enabled) }
}
}
}
diff --git a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceScreenContentTest.kt b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceScreenContentTest.kt
new file mode 100644
index 0000000000..d3c495359d
--- /dev/null
+++ b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceScreenContentTest.kt
@@ -0,0 +1,76 @@
+package com.flipcash.app.balance.internal
+
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import com.flipcash.app.core.tokens.TokenPurpose
+import com.flipcash.app.tokens.ui.SelectTokenViewModel
+import com.getcode.opencode.compose.ExchangeStub
+import com.getcode.opencode.compose.LocalExchange
+import com.getcode.opencode.model.financial.CurrencyCode
+import com.getcode.opencode.model.financial.Rate
+import com.getcode.theme.DesignSystem
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import kotlin.test.assertTrue
+
+/**
+ * Regression coverage for the AddMoneyUX flag removal on the Balance screen. The flag was
+ * `launched = true`, so the empty-wallet call-to-action is now unconditionally the
+ * "Add Money" deposit path — it must never fall back to the old "Discover Currencies"
+ * copy/route, and tapping it must open the deposit options.
+ */
+@RunWith(RobolectricTestRunner::class)
+class BalanceScreenContentTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private var lastEvent: BalanceViewModel.Event? = null
+
+ private fun setEmptyBalanceScreen() {
+ lastEvent = null
+ composeTestRule.setContent {
+ DesignSystem {
+ CompositionLocalProvider(
+ LocalExchange provides ExchangeStub(
+ providedRates = mapOf(CurrencyCode.USD to Rate.oneToOne),
+ context = LocalContext.current,
+ )
+ ) {
+ BalanceScreenContent(
+ tokenState = SelectTokenViewModel.State(
+ purpose = TokenPurpose.Balance,
+ tokens = emptyList(),
+ ),
+ dispatchEvent = { lastEvent = it },
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `empty wallet shows add money cta`() {
+ setEmptyBalanceScreen()
+ composeTestRule.onNodeWithText("Add Money").assertIsDisplayed()
+ }
+
+ @Test
+ fun `empty wallet does not show discover currencies fallback`() {
+ setEmptyBalanceScreen()
+ composeTestRule.onNodeWithText("Discover Currencies").assertDoesNotExist()
+ }
+
+ @Test
+ fun `tapping add money opens deposit options`() {
+ setEmptyBalanceScreen()
+ composeTestRule.onNodeWithText("Add Money").performClick()
+ assertTrue(lastEvent is BalanceViewModel.Event.PresentDepositOptions)
+ }
+}
diff --git a/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceViewModelTest.kt b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceViewModelTest.kt
new file mode 100644
index 0000000000..11e1329b14
--- /dev/null
+++ b/apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceViewModelTest.kt
@@ -0,0 +1,76 @@
+package com.flipcash.app.balance.internal
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.flipcash.app.analytics.StubFlipcashAnalytics
+import com.flipcash.app.core.AppRoute
+import com.flipcash.app.core.MainCoroutineRule
+import com.flipcash.app.core.dispatchers.TestDispatchers
+import com.flipcash.app.funding.PurchaseMethodController
+import com.flipcash.app.userflags.UserFlagsCoordinator
+import com.flipcash.services.internal.model.thirdparty.OnRampProvider
+import com.flipcash.services.user.UserManager
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Regression coverage for the AddMoneyUX flag removal. That flag was `launched = true`
+ * (always on), so the "deposit-first" add-money path is now unconditional: tapping
+ * "Add Money" must always route through [PurchaseMethodController.presentDepositOptions]
+ * and never fall back to the old currency-discovery route.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BalanceViewModelTest {
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ var mainCoroutineRule = MainCoroutineRule(UnconfinedTestDispatcher())
+
+ private val userManager: UserManager = mockk(relaxed = true)
+ private val userFlags: UserFlagsCoordinator = mockk(relaxed = true)
+ private val purchaseMethodController: PurchaseMethodController = mockk(relaxed = true)
+
+ private lateinit var dispatchers: TestDispatchers
+
+ private fun createViewModel() = BalanceViewModel(
+ userManager = userManager,
+ userFlags = userFlags,
+ dispatchers = dispatchers,
+ purchaseMethodController = purchaseMethodController,
+ analytics = StubFlipcashAnalytics(),
+ )
+
+ @Test
+ fun `PresentDepositOptions always routes through add-money deposit options`() =
+ runTest(mainCoroutineRule.dispatcher) {
+ dispatchers = TestDispatchers(testScheduler)
+ val route = mockk(relaxed = true)
+ coEvery { purchaseMethodController.presentDepositOptions(popToRoot = true) } returns route
+
+ val vm = createViewModel()
+ vm.dispatchEvent(BalanceViewModel.Event.PresentDepositOptions)
+ advanceUntilIdle()
+
+ // The removed AddMoneyUX flag used to short-circuit to a plain Deposit route;
+ // the add-money sheet must now always be presented.
+ coVerify(exactly = 1) { purchaseMethodController.presentDepositOptions(popToRoot = true) }
+ }
+
+ @Test
+ fun `OnPreferredOnRampProviderChanged updates state`() {
+ val provider = mockk()
+ val updated = BalanceViewModel.updateStateForEvent(
+ BalanceViewModel.Event.OnPreferredOnRampProviderChanged(provider)
+ )(BalanceViewModel.State())
+ assertEquals(provider, updated.preferredOnRampProvider)
+ }
+}
diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt
index 3410090440..a494a27582 100644
--- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt
+++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt
@@ -16,8 +16,6 @@ import com.flipcash.app.tokens.BalancePoller
import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.app.core.AppRoute
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.features.currencycreator.R
import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.services.controllers.ModerationController
@@ -92,7 +90,6 @@ internal class CurrencyCreatorViewModel @Inject constructor(
private val resources: ResourceHelper,
val contentReader: ContentReader,
val purchaseMethodController: PurchaseMethodController,
- private val featureFlags: FeatureFlagController,
private val currencyCreatorCoordinator: CurrencyCreatorCoordinator,
private val exchange: Exchange,
) : BaseViewModel(
@@ -203,45 +200,42 @@ internal class CurrencyCreatorViewModel @Inject constructor(
eventFlow
.filterIsInstance()
.onEach {
- val addMoney = featureFlags.get(FeatureFlag.AddMoneyUX)
- if (addMoney) {
- // A currency can now be funded by any held currency, not just USDF reserves.
- // No balance at all gates first; having some balance but none large enough to
- // cover the cost gates second.
- val totalCost = stateFlow.value.totalCost
- val balances = tokenCoordinator.tokenBalances.firstOrNull().orEmpty().map { it.balance }
-
- if (balances.none { it.hasDisplayableValue }) {
- // No balance in any currency — require a deposit first.
- BottomBarManager.showInfo(
- title = resources.getString(R.string.title_noBalanceYet),
- message = resources.getString(R.string.description_noBalanceYetToCreate),
- actions = listOf(
- BottomBarAction(
- text = resources.getString(R.string.action_addMoney)
- ) {
- dispatchEvent(Event.PresentDepositOptions)
- },
- ),
- showCancel = true,
- )
- return@onEach
- } else if (balances.none { it > totalCost }) {
- // Has balance, but no single currency covers the cost — require a top-up.
- BottomBarManager.showInfo(
- title = resources.getString(R.string.title_insufficientBalance),
- message = resources.getString(R.string.description_insufficientBalanceToCreate),
- actions = listOf(
- BottomBarAction(
- text = resources.getString(R.string.action_addMoreMoney)
- ) {
- dispatchEvent(Event.PresentDepositOptions)
- },
- ),
- showCancel = true,
- )
- return@onEach
- }
+ // A currency can now be funded by any held currency, not just USDF reserves.
+ // No balance at all gates first; having some balance but none large enough to
+ // cover the cost gates second.
+ val totalCost = stateFlow.value.totalCost
+ val balances = tokenCoordinator.tokenBalances.firstOrNull().orEmpty().map { it.balance }
+
+ if (balances.none { it.hasDisplayableValue }) {
+ // No balance in any currency — require a deposit first.
+ BottomBarManager.showInfo(
+ title = resources.getString(R.string.title_noBalanceYet),
+ message = resources.getString(R.string.description_noBalanceYetToCreate),
+ actions = listOf(
+ BottomBarAction(
+ text = resources.getString(R.string.action_addMoney)
+ ) {
+ dispatchEvent(Event.PresentDepositOptions)
+ },
+ ),
+ showCancel = true,
+ )
+ return@onEach
+ } else if (balances.none { it > totalCost }) {
+ // Has balance, but no single currency covers the cost — require a top-up.
+ BottomBarManager.showInfo(
+ title = resources.getString(R.string.title_insufficientBalance),
+ message = resources.getString(R.string.description_insufficientBalanceToCreate),
+ actions = listOf(
+ BottomBarAction(
+ text = resources.getString(R.string.action_addMoreMoney)
+ ) {
+ dispatchEvent(Event.PresentDepositOptions)
+ },
+ ),
+ showCancel = true,
+ )
+ return@onEach
}
dispatchEvent(Event.AdvanceFromInfo)
@@ -250,9 +244,6 @@ internal class CurrencyCreatorViewModel @Inject constructor(
eventFlow
.filterIsInstance()
.mapNotNull {
- if (!featureFlags.get(FeatureFlag.AddMoneyUX)) {
- return@mapNotNull AppRoute.Transfers.Deposit(showOtherOptions = false)
- }
// popToRoot = false so finishing the deposit returns to the currency
// creator (which pushed this flow) rather than tearing down the whole
// sheet and losing the user's place in the flow.
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
index 2928b94bf9..d23fb6e8d3 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
@@ -90,17 +90,13 @@ internal class SendFlowViewModel @Inject constructor(
init {
combine(
userManager.state,
- featureFlags.observe(FeatureFlag.PhoneNumberSend),
featureFlags.observe(FeatureFlag.ContactPickerMode),
contactCoordinator.state,
chatCoordinator.feed(ChatType.CONTACT_DM),
- ) { userState, phoneNumberSendFlag, contactPickerMode, contactState, chats ->
+ ) { userState, contactPickerMode, contactState, chats ->
val hasLinkedPhone = userState.userProfile?.verifiedPhoneNumber != null
- val phoneNumberSendEnabled = phoneNumberSendFlag ||
- userState.flags?.enablePhoneNumberSend == true
val hasContacts = contactState.contacts.isNotEmpty()
- val needsContacts =
- phoneNumberSendEnabled && !hasContacts && !contactState.hasEverSynced
+ val needsContacts = !hasContacts && !contactState.hasEverSynced
val steps = buildList {
if (!hasLinkedPhone) add(SendStep.PhoneGate)
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
index ebbf9b918f..72c68f4711 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
@@ -72,7 +72,6 @@ private fun TokenDiscoveryScreenContent(
state = listState,
tokens = tokens,
padding = padding,
- showGradientAtEnd = !state.createEnabled,
dispatch = dispatch
)
}
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryViewModel.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryViewModel.kt
index a9f5335716..bd13e01a2e 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryViewModel.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryViewModel.kt
@@ -3,8 +3,6 @@ package com.flipcash.app.discovery.internal
import androidx.lifecycle.viewModelScope
import com.flipcash.app.core.data.Loadable
import com.flipcash.app.core.extensions.onResult
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.features.discovery.R
import com.getcode.opencode.controllers.CurrencyController
@@ -43,7 +41,6 @@ internal class TokenDiscoveryViewModel @Inject constructor(
private val currencyController: CurrencyController,
private val userFlags: UserFlagsCoordinator,
private val resources: ResourceHelper,
- featureFlags: FeatureFlagController,
dispatchers: DispatcherProvider,
) : BaseViewModel(
initialState = State(),
@@ -52,14 +49,12 @@ internal class TokenDiscoveryViewModel @Inject constructor(
) {
data class State(
- val createEnabled: Boolean = false,
val category: DiscoverCategory? = null,
val tokens: Loadable> = Loadable.Loading(),
val minimumHolderAmount: Fiat = 10.toFiat(),
)
sealed interface Event {
- data class OnCreateAllowed(val enabled: Boolean) : Event
data class OnMinimumHolderAmountChanged(val amount: Fiat): Event
data object LearnAboutLeaderboard: Event
data class OnCategorySelected(
@@ -81,10 +76,6 @@ internal class TokenDiscoveryViewModel @Inject constructor(
.onEach { dispatchEvent(Event.OnMinimumHolderAmountChanged(it)) }
.launchIn(viewModelScope)
- featureFlags.observe(FeatureFlag.CurrencyCreator)
- .onEach { dispatchEvent(Event.OnCreateAllowed(it)) }
- .launchIn(viewModelScope)
-
eventFlow
.filterIsInstance()
.onEach { dispatchEvent(Event.OnTokensUpdated(Loadable.Loading())) }
@@ -152,10 +143,6 @@ internal class TokenDiscoveryViewModel @Inject constructor(
state.copy(minimumHolderAmount = event.amount)
}
- is Event.OnCreateAllowed -> { state ->
- state.copy(createEnabled = event.enabled)
- }
-
is Event.OnTokensUpdated -> { state ->
state.copy(tokens = event.loadable)
}
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
index 91ef292d9c..b3bd39014a 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
@@ -47,12 +47,11 @@ import com.getcode.ui.utils.sheetResignmentBehavior
internal fun TokenLeaderboard(
category: DiscoverCategory?,
tokens: Loadable>,
- showGradientAtEnd: Boolean,
padding: PaddingValues,
state: LazyListState,
dispatch: (TokenDiscoveryViewModel.Event) -> Unit
) {
- val reduceBottomPadding = if (showGradientAtEnd) 0.dp else CodeTheme.dimens.grid.x4
+ val reduceBottomPadding = CodeTheme.dimens.grid.x4
LazyColumn(
modifier = Modifier
.fillMaxSize()
@@ -61,7 +60,7 @@ internal fun TokenLeaderboard(
state,
color = CodeTheme.colors.background,
isLongGradient = true,
- showAtEnd = showGradientAtEnd,
+ showAtEnd = false,
)
.addIf(tokens.isLoaded()) {
Modifier.sheetResignmentBehavior(state)
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 a9f0e78bdd..4d53e9d624 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
@@ -96,12 +96,11 @@ import kotlin.time.Duration.Companion.milliseconds
* Same as (1) but initialStack resumes at the AccessKey or Purchase step.
* ```
*
- * ¹ Contact permission is shown only when [FeatureFlag.PhoneNumberSend] is enabled
- * **and** [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].
- * ² Phone verification is shown only when [FeatureFlag.OnboardingPhoneVerification] is enabled
- * and no phone is linked. Skipped entirely when the flag is off.
+ * ¹ 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].
+ * ² Phone verification is shown only when no phone is linked.
* Uses `target` to replace the nav stack with AccessKey on success.
*/
@Composable
@@ -146,15 +145,10 @@ private fun PermissionsPhaseFlowHost(
val featureFlags = LocalFeatureFlags.current
val userManager = LocalUserManager.current
- val userFlags = userManager?.state?.collectAsStateWithLifecycle()?.value?.flags
- val phoneNumberSendFlagEnabled by featureFlags.observe(FeatureFlag.PhoneNumberSend).collectAsStateWithLifecycle()
- val phoneNumberSendEnabled = remember(userFlags?.enablePhoneNumberSend, phoneNumberSendFlagEnabled) {
- phoneNumberSendFlagEnabled || userFlags?.enablePhoneNumberSend == true
- }
val contactPickerMode by featureFlags.observe(FeatureFlag.ContactPickerMode).collectAsStateWithLifecycle()
val permissionsSteps = buildList {
- if (!route.skipContacts && phoneNumberSendEnabled && !contactPickerMode) add(OnboardingStep.ContactPermission)
+ if (!route.skipContacts && !contactPickerMode) add(OnboardingStep.ContactPermission)
add(OnboardingStep.NotificationPermission)
}
@@ -166,7 +160,7 @@ private fun PermissionsPhaseFlowHost(
val notificationsGranted = !notificationConfig.requiresRuntimeRequest ||
checker.isGranted(notificationConfig.permission)
when {
- !route.skipContacts && phoneNumberSendEnabled && !contactsGranted -> 0
+ !route.skipContacts && !contactsGranted -> 0
!notificationsGranted -> permissionsSteps.indexOfFirst {
it is OnboardingStep.NotificationPermission
}.coerceAtLeast(0)
diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt
index 0a518ad1c1..f731eb8da9 100644
--- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt
+++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt
@@ -4,8 +4,6 @@ import androidx.lifecycle.viewModelScope
import com.flipcash.app.analytics.Button
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.auth.AuthManager
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.features.login.R
import com.flipcash.services.controllers.AccountController
import com.flipcash.services.user.UserManager
@@ -16,7 +14,6 @@ import com.flipcash.libs.coroutines.DispatcherProvider
import com.getcode.view.BaseViewModel
import com.getcode.view.LoadingSuccessState
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
@@ -38,7 +35,6 @@ class LoginViewModel @Inject constructor(
private val resources: ResourceHelper,
private val analytics: FlipcashAnalyticsService,
userManager: UserManager,
- featureFlags: FeatureFlagController,
dispatchers: DispatcherProvider,
) : BaseViewModel(
initialState = State(),
@@ -72,16 +68,11 @@ class LoginViewModel @Inject constructor(
private val createInFlight = AtomicBoolean(false)
init {
- combine(
- userManager.state,
- featureFlags.observe(FeatureFlag.OnboardingPhoneVerification),
- ) { userState, phoneVerificationFlag ->
- val enabled = phoneVerificationFlag || userState.flags?.enablePhoneNumberSend == true
- val hasLinkedPhone = userState.userProfile?.verifiedPhoneNumber != null
- enabled && !hasLinkedPhone
- }.onEach { needed ->
- dispatchEvent(Event.PhoneVerificationUpdated(needed))
- }.launchIn(viewModelScope)
+ userManager.state
+ .map { it.userProfile?.verifiedPhoneNumber == null }
+ .onEach { needed ->
+ dispatchEvent(Event.PhoneVerificationUpdated(needed))
+ }.launchIn(viewModelScope)
eventFlow
.filterIsInstance()
diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt
index 0b590a4304..256ee5401c 100644
--- a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt
+++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt
@@ -5,7 +5,6 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.auth.AuthManager
import com.flipcash.app.core.MainCoroutineRule
import com.flipcash.app.core.dispatchers.TestDispatchers
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.services.controllers.AccountController
import com.flipcash.services.user.UserManager
import com.getcode.manager.BottomBarManager
@@ -48,7 +47,6 @@ class LoginViewModelCreateAccountTest {
private val resources = FakeResourceHelper()
private val analytics: FlipcashAnalyticsService = mockk(relaxed = true)
private val userManager: UserManager = mockk(relaxed = true)
- private val featureFlags: FeatureFlagController = mockk(relaxed = true)
private lateinit var dispatchers: TestDispatchers
@@ -71,7 +69,6 @@ class LoginViewModelCreateAccountTest {
resources = resources,
analytics = analytics,
userManager = userManager,
- featureFlags = featureFlags,
dispatchers = dispatchers,
)
diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt
index 0072f29cad..11279ad636 100644
--- a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt
+++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt
@@ -3,7 +3,6 @@ package com.flipcash.app.login.router
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.auth.AuthManager
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.core.MainCoroutineRule
import com.flipcash.app.core.dispatchers.TestDispatchers
import com.flipcash.services.controllers.AccountController
@@ -43,7 +42,6 @@ class LoginViewModelErrorTest {
private val resources = FakeResourceHelper()
private val analytics: FlipcashAnalyticsService = mockk(relaxed = true)
private val userManager: UserManager = mockk(relaxed = true)
- private val featureFlags: FeatureFlagController = mockk(relaxed = true)
private lateinit var dispatchers: TestDispatchers
@@ -68,7 +66,6 @@ class LoginViewModelErrorTest {
resources = resources,
analytics = analytics,
userManager = userManager,
- featureFlags = featureFlags,
dispatchers = dispatchers,
)
}
diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelPhoneVerificationTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelPhoneVerificationTest.kt
new file mode 100644
index 0000000000..acf7b4afee
--- /dev/null
+++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelPhoneVerificationTest.kt
@@ -0,0 +1,108 @@
+package com.flipcash.app.login.router
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.flipcash.app.analytics.FlipcashAnalyticsService
+import com.flipcash.app.auth.AuthManager
+import com.flipcash.app.core.MainCoroutineRule
+import com.flipcash.app.core.dispatchers.TestDispatchers
+import com.flipcash.services.controllers.AccountController
+import com.flipcash.services.models.UserProfile
+import com.flipcash.services.user.UserManager
+import com.getcode.manager.BottomBarManager
+import com.getcode.util.resources.FakeResourceHelper
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+/**
+ * Regression coverage for the OnboardingPhoneVerification / PhoneNumberSend flag removal.
+ * Both flags were `launched = true`, so phone verification is now driven purely by whether
+ * the account has a verified phone number — the flag no longer gates it.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class LoginViewModelPhoneVerificationTest {
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ var mainCoroutineRule = MainCoroutineRule(UnconfinedTestDispatcher())
+
+ private val authManager: AuthManager = mockk(relaxed = true)
+ private val accounts: AccountController = mockk(relaxed = true)
+ private val resources = FakeResourceHelper()
+ private val analytics: FlipcashAnalyticsService = mockk(relaxed = true)
+ private val userManager: UserManager = mockk(relaxed = true)
+
+ private lateinit var dispatchers: TestDispatchers
+
+ @Before
+ fun setUp() {
+ BottomBarManager.clear()
+ mockkStatic(android.util.Base64::class)
+ every { android.util.Base64.encodeToString(any(), any()) } answers {
+ java.util.Base64.getEncoder().encodeToString(firstArg())
+ }
+ }
+
+ @After
+ fun tearDown() {
+ BottomBarManager.clear()
+ unmockkStatic(android.util.Base64::class)
+ }
+
+ private fun stubProfileWithPhone(verifiedPhoneNumber: String?) {
+ val profile = mockk(relaxed = true) {
+ every { this@mockk.verifiedPhoneNumber } returns verifiedPhoneNumber
+ }
+ val state = mockk(relaxed = true) {
+ every { userProfile } returns profile
+ }
+ every { userManager.state } returns MutableStateFlow(state)
+ }
+
+ private fun createViewModel() = LoginViewModel(
+ authManager = authManager,
+ accounts = accounts,
+ resources = resources,
+ analytics = analytics,
+ userManager = userManager,
+ dispatchers = dispatchers,
+ )
+
+ @Test
+ fun `phone verification needed when no verified phone linked`() =
+ runTest(mainCoroutineRule.dispatcher) {
+ dispatchers = TestDispatchers(testScheduler)
+ stubProfileWithPhone(verifiedPhoneNumber = null)
+
+ val vm = createViewModel()
+ advanceUntilIdle()
+
+ assertTrue(vm.stateFlow.value.needsPhoneVerification)
+ }
+
+ @Test
+ fun `phone verification not needed when a verified phone is linked`() =
+ runTest(mainCoroutineRule.dispatcher) {
+ dispatchers = TestDispatchers(testScheduler)
+ stubProfileWithPhone(verifiedPhoneNumber = "+15005550000")
+
+ val vm = createViewModel()
+ advanceUntilIdle()
+
+ assertFalse(vm.stateFlow.value.needsPhoneVerification)
+ }
+}
diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt
index 0ef36b23dd..d9fe6a18bf 100644
--- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt
+++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenContent.kt
@@ -97,11 +97,7 @@ internal fun MenuScreenContent(viewModel: MenuScreenViewModel) {
) {
TileButton(
modifier = Modifier.weight(1f),
- text = if (state.addMoneyUxEnabled) {
- stringResource(R.string.action_addMoney)
- } else {
- stringResource(R.string.action_deposit)
- },
+ text = stringResource(R.string.action_addMoney),
icon = painterResource(R.drawable.ic_menu_deposit)
) {
viewModel.dispatchEvent(Event.PresentDepositOptions)
@@ -109,11 +105,7 @@ internal fun MenuScreenContent(viewModel: MenuScreenViewModel) {
TileButton(
modifier = Modifier.weight(1f),
- text = if (state.addMoneyUxEnabled) {
- stringResource(R.string.action_withdrawMoney)
- } else {
- stringResource(R.string.action_withdraw)
- },
+ text = stringResource(R.string.action_withdrawMoney),
icon = painterResource(R.drawable.ic_menu_withdraw)
) {
navigator.push(AppRoute.Transfers.Withdrawal())
diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt
index 91d6aeeada..62d06baa8d 100644
--- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt
+++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt
@@ -8,7 +8,6 @@ import com.flipcash.app.core.AppRoute
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
@@ -70,7 +69,6 @@ internal class MenuScreenViewModel @Inject constructor(
val unlockedBetaFeaturesManually: Boolean = false,
val appVersionInfo: VersionInfo = VersionInfo(),
val releaseTrack: String = "",
- val addMoneyUxEnabled: Boolean = false,
)
sealed interface Event {
@@ -169,11 +167,6 @@ internal class MenuScreenViewModel @Inject constructor(
.filterIsInstance()
.mapNotNull {
analytics.addMoneyOpened(Analytics.AddMoneySource.Menu)
- val depositFirstUx = featureFlags.get(FeatureFlag.AddMoneyUX)
- if (!depositFirstUx) {
- return@mapNotNull AppRoute.Transfers.Deposit()
- }
-
purchaseMethodController.presentDepositOptions(popToRoot = true)
}.onEach { route -> dispatchEvent(Event.OpenScreen(route)) }
.launchIn(viewModelScope)
@@ -263,7 +256,6 @@ internal class MenuScreenViewModel @Inject constructor(
flags = event.flags
),
flags = event.flags,
- addMoneyUxEnabled = event.flags.find { it.flag == FeatureFlag.AddMoneyUX }?.enabled ?: true,
)
}
}
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
index e3d777eca2..6b99e611a6 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
@@ -628,10 +628,9 @@ internal class ChatViewModel @Inject constructor(
// backs the chat. The final send branches on that type (see Event.OnSendRequested).
.filter { stateFlow.value.participant != null }
.onEach {
- val addMoney = featureFlags.get(FeatureFlag.AddMoneyUX)
if (!tokenCoordinator.hasGiveableBalance()) {
if (!tokenCoordinator.hasBalance()) {
- presentAddMoney(addMoney)
+ presentAddMoney()
} else {
presentDiscoverCurrencies()
}
@@ -798,29 +797,15 @@ internal class ChatViewModel @Inject constructor(
}
}
- private fun presentAddMoney(addMoneyEnabled: Boolean) {
- val message = if (addMoneyEnabled) {
- resources.getString(R.string.description_noBalanceYetToSend)
- } else {
- resources.getString(R.string.description_noBalanceYetDiscover)
- }
- val cta = if (addMoneyEnabled) {
- resources.getString(R.string.action_addMoney)
- } else {
- resources.getString(R.string.action_discover)
- }
+ private fun presentAddMoney() {
BottomBarManager.showInfo(
title = resources.getString(R.string.title_noBalanceYet),
- message = message,
+ message = resources.getString(R.string.description_noBalanceYetToSend),
actions = listOf(
BottomBarAction(
- text = cta
+ text = resources.getString(R.string.action_addMoney)
) {
- if (addMoneyEnabled) {
- dispatchEvent(Event.PresentDepositOptions)
- } else {
- dispatchEvent(Event.OpenScreen(AppRoute.Token.Discovery))
- }
+ dispatchEvent(Event.PresentDepositOptions)
},
),
showCancel = true,
diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/ui/components/ScannerNavigationBar.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/ui/components/ScannerNavigationBar.kt
index d6c1b54ff8..1ada39ed18 100644
--- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/ui/components/ScannerNavigationBar.kt
+++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/ui/components/ScannerNavigationBar.kt
@@ -31,11 +31,10 @@ internal fun ScannerNavigationBar(
NavBarConfig.deserialize(navBarConfigString)
}
- val effectiveConfig = remember(config, state.isPhoneNumberSendEnabled, state.isTippingEnabled) {
+ val effectiveConfig = remember(config, state.isTippingEnabled) {
val buttons = config.order
.filter { option ->
when (option) {
- NavBarButton.Send -> state.isPhoneNumberSendEnabled
NavBarButton.Tips -> state.isTippingEnabled
else -> true
}
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt
index 0a9458730e..6f398bfd2c 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt
@@ -210,7 +210,6 @@ private fun TokenInfoScreen(
.fillParentMaxWidth(),
contentPadding = PaddingValues(horizontal = CodeTheme.dimens.inset),
marketCap = mcap,
- chartEnabled = state.marketCapChartEnabled,
selectedPeriod = state.selectedPeriod,
rawHistoricalData = loadable,
onRetry = {
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt
index cc458e42a6..ed8a22f918 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt
@@ -71,7 +71,6 @@ private sealed interface MarketCapLabelState {
@Composable
internal fun MarketCapSection(
marketCap: Fiat,
- chartEnabled: Boolean,
rawHistoricalData: Loadable>,
selectedPeriod: Period,
modifier: Modifier = Modifier,
@@ -144,69 +143,67 @@ internal fun MarketCapSection(
color = CodeTheme.colors.textMain,
)
- if (chartEnabled) {
- AnimatedContent(
- modifier = Modifier
- .padding(
- start = contentPadding.calculateStartPadding(),
- top = CodeTheme.dimens.grid.x1,
- )
- .height(IntrinsicSize.Max),
- targetState = labelState,
- label = "MarketCapLabel",
- transitionSpec = {
- fadeIn().togetherWith(fadeOut()).using(SizeTransform(clip = false))
- },
- contentKey = { state ->
- when (state) {
- is MarketCapLabelState.Change -> "change"
- is MarketCapLabelState.Highlighted -> "highlighted"
- MarketCapLabelState.Hidden -> "hidden"
- }
- }
- ) { state ->
+ AnimatedContent(
+ modifier = Modifier
+ .padding(
+ start = contentPadding.calculateStartPadding(),
+ top = CodeTheme.dimens.grid.x1,
+ )
+ .height(IntrinsicSize.Max),
+ targetState = labelState,
+ label = "MarketCapLabel",
+ transitionSpec = {
+ fadeIn().togetherWith(fadeOut()).using(SizeTransform(clip = false))
+ },
+ contentKey = { state ->
when (state) {
- is MarketCapLabelState.Change -> MarketCapChangeLabel(
- change = state.change,
- period = state.period,
- )
-
- is MarketCapLabelState.Highlighted -> HighlightedPointLabel(
- modifier = Modifier.padding(bottom = 4.dp),
- point = state.point,
- period = state.period,
- )
-
- MarketCapLabelState.Hidden -> {
- MarketCapChangeLabel(
- modifier = Modifier.alpha(0f),
- change = Fiat.Zero,
- period = selectedPeriod,
- )
- }
+ is MarketCapLabelState.Change -> "change"
+ is MarketCapLabelState.Highlighted -> "highlighted"
+ MarketCapLabelState.Hidden -> "hidden"
}
}
+ ) { state ->
+ when (state) {
+ is MarketCapLabelState.Change -> MarketCapChangeLabel(
+ change = state.change,
+ period = state.period,
+ )
- MarketCapChart(
- modifier = Modifier
- .fillMaxWidth()
- .requiredHeight(240.dp),
- chartPadding = PaddingValues(end = contentPadding.calculateEndPadding()),
- periodPadding = PaddingValues(
- start = contentPadding.calculateStartPadding(),
- end = contentPadding.calculateEndPadding(),
- ),
- data = data,
- currentValue = marketCap.decimalValue,
- trendType = TrendType.FirstVsLast,
- selectedPeriod = selectedPeriod,
- onPointHighlighted = { highlightedCapPoint = it },
- onPeriodSelected = onPeriodSelected,
- placeholder = {
- MarketCapChartPlaceholder(rawHistoricalData, onRetry)
+ is MarketCapLabelState.Highlighted -> HighlightedPointLabel(
+ modifier = Modifier.padding(bottom = 4.dp),
+ point = state.point,
+ period = state.period,
+ )
+
+ MarketCapLabelState.Hidden -> {
+ MarketCapChangeLabel(
+ modifier = Modifier.alpha(0f),
+ change = Fiat.Zero,
+ period = selectedPeriod,
+ )
}
- )
+ }
}
+
+ MarketCapChart(
+ modifier = Modifier
+ .fillMaxWidth()
+ .requiredHeight(240.dp),
+ chartPadding = PaddingValues(end = contentPadding.calculateEndPadding()),
+ periodPadding = PaddingValues(
+ start = contentPadding.calculateStartPadding(),
+ end = contentPadding.calculateEndPadding(),
+ ),
+ data = data,
+ currentValue = marketCap.decimalValue,
+ trendType = TrendType.FirstVsLast,
+ selectedPeriod = selectedPeriod,
+ onPointHighlighted = { highlightedCapPoint = it },
+ onPeriodSelected = onPeriodSelected,
+ placeholder = {
+ MarketCapChartPlaceholder(rawHistoricalData, onRetry)
+ }
+ )
}
}
@@ -363,7 +360,6 @@ private fun Preview_MarketCapSection() {
24.44.toFiat(),
rawHistoricalData = Loadable.Error(""),
selectedPeriod = Period.All,
- chartEnabled = true,
onRetry = {},
onPeriodSelected = {}
)
diff --git a/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt b/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt
index d172639795..d67e8f3838 100644
--- a/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt
+++ b/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt
@@ -5,7 +5,6 @@ import com.flipcash.app.appsettings.AppSettingsCoordinator
import com.flipcash.app.auth.internal.credentials.LookupResult
import com.flipcash.app.auth.internal.credentials.PassphraseCredentialManager
import com.flipcash.app.contacts.ContactCoordinator
-import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.persistence.PersistenceProvider
import com.flipcash.app.push.PushTokenProvider
@@ -258,7 +257,6 @@ class AuthManager @Inject constructor(
// If phone verification is required but not yet completed, onboarding
// should resume at the phone-verification step (which precedes the
// access key) rather than jumping ahead to the access key.
- val phoneVerificationEnabled = featureFlags.get(FeatureFlag.OnboardingPhoneVerification)
val phoneUnverified = userManager.profile?.verifiedPhoneNumber == null
if (flags != null) {
userManager.set(flags)
@@ -266,7 +264,7 @@ class AuthManager @Inject constructor(
userManager.set(AuthState.Ready)
} else {
val resumePoint = when {
- !seenAccessKey && (phoneVerificationEnabled || flags.enablePhoneNumberSend) && phoneUnverified ->
+ !seenAccessKey && phoneUnverified ->
AuthState.ResumePoint.PhoneNumber
!seenAccessKey -> AuthState.ResumePoint.AccessKey
flags.requiresIapForRegistration -> AuthState.ResumePoint.AccessKeyThenPurchase
@@ -282,7 +280,7 @@ class AuthManager @Inject constructor(
} else {
val resumePoint = when {
seenAccessKey -> AuthState.ResumePoint.PostAccessKey
- phoneVerificationEnabled && phoneUnverified -> AuthState.ResumePoint.PhoneNumber
+ phoneUnverified -> AuthState.ResumePoint.PhoneNumber
else -> AuthState.ResumePoint.AccessKey
}
trace(tag = "Onboarding", message = "Resuming onboarding at $resumePoint (flags unavailable)", type = TraceType.Process)
diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt
index e0453af629..b25f995535 100644
--- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt
+++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt
@@ -5,8 +5,6 @@ package com.flipcash.shared.chat.internal
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.user.UserManager
@@ -32,14 +30,12 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -65,8 +61,7 @@ import kotlin.time.Duration.Companion.seconds
* All cross-delegate wiring is visible in one place.
* - **Lifecycle methods** — [onStart]/[onStop] are inherently cross-cutting
* (stream connect/disconnect, heartbeat start/stop, active-chat save/restore).
- * - **Flow observers** — network reconnect and feature-flag transitions that
- * gate whether the chat subsystem should be active.
+ * - **Flow observers** — network reconnect re-syncing the chat feed.
*
* Delegates require [initialize] with a shared [CoroutineScope] before use;
* this happens in [onUserLoggedIn].
@@ -79,7 +74,6 @@ class RealChatCoordinator @Inject constructor(
private val messagingDelegate: MessagingDelegate,
private val stateHolder: ChatStateHolder,
private val userManager: UserManager,
- private val featureFlags: FeatureFlagController,
private val networkObserver: NetworkConnectivityListener,
private val dispatchers: DispatcherProvider,
) : ChatCoordinator,
@@ -101,7 +95,6 @@ class RealChatCoordinator @Inject constructor(
private var supervisorJob = SupervisorJob()
private var scope = CoroutineScope(dispatchers.IO + supervisorJob)
private val cluster = MutableStateFlow(null)
- private var flagObserverJob: Job? = null
private var networkObserverJob: Job? = null
private var backgroundedActiveChat: ChatId? = null
@@ -127,7 +120,6 @@ class RealChatCoordinator @Inject constructor(
feedDelegate.syncFeed()
eventStreamDelegate.open()
eventStreamDelegate.startHeartbeat { feedDelegate.syncFeed() }
- observeFeatureFlag()
}
// endregion
@@ -172,7 +164,6 @@ class RealChatCoordinator @Inject constructor(
.filter { it.connected }
.debounce(1.seconds)
.onEach {
- if (!isChatEnabled()) return@onEach
trace(tag = TAG, message = "Network connected, re-syncing chat feed", type = TraceType.Process)
feedDelegate.syncFeed()
eventStreamDelegate.open()
@@ -186,7 +177,7 @@ class RealChatCoordinator @Inject constructor(
backgroundedActiveChat = null
}
scope.launch {
- if (cluster.value != null && isChatEnabled()) {
+ if (cluster.value != null) {
trace(tag = TAG, message = "Lifecycle resumed, syncing chat feed", type = TraceType.Process)
feedDelegate.syncFeed()
eventStreamDelegate.open()
@@ -210,7 +201,6 @@ class RealChatCoordinator @Inject constructor(
eventStreamDelegate.stopHeartbeat()
eventStreamDelegate.close()
feedDelegate.cancelJobs()
- flagObserverJob?.cancel()
networkObserverJob?.cancel()
stateHolder.reset()
eventStreamDelegate.clearAll()
@@ -222,32 +212,4 @@ class RealChatCoordinator @Inject constructor(
// endregion
- // region Internal
-
- private suspend fun isChatEnabled(): Boolean {
- val featureFlag = featureFlags.get(FeatureFlag.PhoneNumberSend)
- val serverFlag = userManager.state.value.flags?.enablePhoneNumberSend == true
- return featureFlag || serverFlag
- }
-
- private fun observeFeatureFlag() {
- flagObserverJob?.cancel()
- flagObserverJob = combine(
- featureFlags.observe(FeatureFlag.PhoneNumberSend),
- userManager.state.map { it.flags?.enablePhoneNumberSend == true },
- ) { featureFlag, serverFlag -> featureFlag || serverFlag }
- .distinctUntilChanged()
- .filter { it }
- .onEach {
- if (cluster.value != null) {
- trace(tag = TAG, message = "Chat feature enabled, syncing feed", type = TraceType.Process)
- feedDelegate.syncFeed()
- eventStreamDelegate.open()
- eventStreamDelegate.startHeartbeat { feedDelegate.syncFeed() }
- }
- }
- .launchIn(scope)
- }
-
- // endregion
}
diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt
index 9d07ffa9f9..dc8b210d7b 100644
--- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt
+++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt
@@ -1,6 +1,5 @@
package com.flipcash.shared.chat
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.persistence.sources.ChatMemberDataSource
import com.flipcash.app.persistence.sources.ChatMessageDataSource
import com.flipcash.app.persistence.sources.ChatMetadataDataSource
@@ -125,7 +124,6 @@ class ChatCoordinatorEagerBalanceTest {
messagingDelegate = messagingDelegate,
stateHolder = stateHolder,
userManager = userManager,
- featureFlags = mockk(relaxed = true),
networkObserver = mockk(relaxed = true),
dispatchers = testDispatchers,
)
diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt
index dc1c72167d..a9ee5dae71 100644
--- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt
+++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt
@@ -1,6 +1,5 @@
package com.flipcash.shared.chat
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.persistence.sources.ChatMemberDataSource
import com.flipcash.app.persistence.sources.ChatMessageDataSource
import com.flipcash.app.persistence.sources.ChatMetadataDataSource
@@ -130,7 +129,6 @@ class ChatCoordinatorEventsTest {
messagingDelegate = messagingDelegate,
stateHolder = stateHolder,
userManager = userManager,
- featureFlags = mockk(relaxed = true),
networkObserver = mockk(relaxed = true),
dispatchers = testDispatchers,
)
diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt
index a0dbfe4177..588b646c73 100644
--- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt
+++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt
@@ -19,8 +19,6 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.contacts.device.DeviceContactLookup
import com.flipcash.app.contacts.device.PickedContactData
import com.flipcash.app.contacts.device.ScopeAwareContactReader
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.phone.PhoneUtils
import com.flipcash.app.contacts.sync.ContactChecksum
import com.flipcash.app.core.contacts.DeviceContact
@@ -82,7 +80,6 @@ class ContactCoordinator @Inject constructor(
private val phoneUtils: PhoneUtils,
private val contactDataSource: ContactDataSource,
private val userManager: UserManager,
- private val featureFlagController: FeatureFlagController,
private val dispatchers: DispatcherProvider,
private val analytics: FlipcashAnalyticsService,
) : SessionListener, DefaultLifecycleObserver {
@@ -280,11 +277,6 @@ class ContactCoordinator @Inject constructor(
scope.launch {
if (!linkMutex.tryLock()) return@launch
try {
- val featureFlag = featureFlagController.get(FeatureFlag.PhoneNumberSend)
- val serverFlag = userManager.state.value.flags?.enablePhoneNumberSend == true
- val enabled = featureFlag || serverFlag
- if (!enabled) return@launch
-
val alreadyLinked = contactPrefs.data
.map { it[KEY_LINKED_FOR_PAYMENT] ?: false }
.first()
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 b85ebd2d17..707aa52945 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
@@ -53,15 +53,6 @@ sealed interface FeatureFlag {
override val persistLogOut: Boolean = false
}
- @FeatureFlagMarker
- data object WelcomeBonusBill: FeatureFlag {
- override val key: String = "welcome_bonus_bill_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object TransactionDetails: FeatureFlag {
override val key: String = "transaction_details_enabled"
@@ -71,69 +62,6 @@ sealed interface FeatureFlag {
override val persistLogOut: Boolean = false
}
- @FeatureFlagMarker
- data object Pools: FeatureFlag {
- override val key: String = "pools_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
- @FeatureFlagMarker
- data object OnRamp: FeatureFlag {
- override val key: String = "onramp_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
- @FeatureFlagMarker
- data object BillCustomizer: FeatureFlag {
- override val key: String = "bill_customizer_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
- @FeatureFlagMarker
- data object CurrencyCreator: FeatureFlag {
- override val key: String = "currency_creator_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
- @FeatureFlagMarker
- data object CashReservesEnabled: FeatureFlag {
- override val key: String = "cash_reserves_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
- @FeatureFlagMarker
- data object MarketCapChart: FeatureFlag {
- override val key: String = "market_cap_chart_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = true
- }
-
- @FeatureFlagMarker
- data object CoinbaseOnRamp: FeatureFlag {
- override val key: String = "coinbase_onramp_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object CoinbaseOnRampSandbox: FeatureFlag {
override val key: String = "coinbase_onramp_sandbox_enabled"
@@ -143,15 +71,6 @@ sealed interface FeatureFlag {
override val persistLogOut: Boolean = false
}
- @FeatureFlagMarker
- data object TokenDiscovery: FeatureFlag {
- override val key: String = "token_discovery_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object BillTextures : FeatureFlag {
override val key: String = "bill_textures_enabled"
@@ -161,15 +80,6 @@ sealed interface FeatureFlag {
override val persistLogOut: Boolean = false
}
- @FeatureFlagMarker
- data object DepositUsdc: FeatureFlag {
- override val key: String = "deposit_usdc_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object BackgroundReset : FeatureFlag {
override val key: String = "idle_reset"
@@ -190,35 +100,6 @@ sealed interface FeatureFlag {
override val persistLogOut: Boolean = true
}
- @FeatureFlagMarker
- data object PhoneNumberSend : FeatureFlag {
- override val key: String = "phone_number_send_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = true
- override val minTrack: FeatureTrack = FeatureTrack.Production
- }
-
- @FeatureFlagMarker
- data object OnboardingPhoneVerification : FeatureFlag {
- override val key: String = "phone_verification_onboarding_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = true
- override val onboarding: Boolean = true
- }
-
- @FeatureFlagMarker
- data object Messenger : FeatureFlag {
- override val key: String = "messenger_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object GiveUsdf: FeatureFlag {
override val key: String = "give_usdf_enabled"
@@ -239,15 +120,6 @@ sealed interface FeatureFlag {
override val defaultOption: String get() = default.serialize()
}
- @FeatureFlagMarker
- data object AddMoneyUX: FeatureFlag {
- override val key: String = "deposit_first_ux_enabled"
- override val default: Boolean = true
- override val launched: Boolean = true
- override val visible: Boolean = true
- override val persistLogOut: Boolean = false
- }
-
@FeatureFlagMarker
data object ShowNetworkState: FeatureFlag {
override val key: String = "show_network_state_enabled"
@@ -300,27 +172,13 @@ val FeatureFlag<*>.title: String
get() = when (this) {
is FeatureFlag.CredentialManager -> "Credential Manager"
FeatureFlag.VibrateOnScan -> "Vibrate on Scan"
- FeatureFlag.WelcomeBonusBill -> "Receive Welcome Bonus as a Bill"
FeatureFlag.TransactionDetails -> "Transaction Details"
- FeatureFlag.Pools -> "Betting Pools"
- FeatureFlag.OnRamp -> "Onramp"
- FeatureFlag.BillCustomizer -> "Bill Customizer"
- FeatureFlag.CashReservesEnabled -> "Cash Reserves"
- FeatureFlag.MarketCapChart -> "Market Cap Chart"
- FeatureFlag.CoinbaseOnRamp -> "Coinbase Onramp"
FeatureFlag.CoinbaseOnRampSandbox -> "Coinbase Onramp Sandbox"
- FeatureFlag.TokenDiscovery -> "Token Discovery"
- FeatureFlag.CurrencyCreator -> "Currency Creator"
FeatureFlag.BillTextures -> "Bill Textures"
- FeatureFlag.DepositUsdc -> "Deposit USDC"
FeatureFlag.BackgroundReset -> "Background Reset"
FeatureFlag.ContactPickerMode -> "Contact Picker Mode"
- FeatureFlag.PhoneNumberSend -> "Send Cash"
- FeatureFlag.OnboardingPhoneVerification -> "Onboarding Phone Verification"
- FeatureFlag.Messenger -> "Messenger"
FeatureFlag.NavBar -> "Navigation Bar"
FeatureFlag.GiveUsdf -> "Give/Send USDF"
- FeatureFlag.AddMoneyUX -> "Add Money UX"
FeatureFlag.ShowNetworkState -> "Network Offline Indicator"
FeatureFlag.Tipping -> "Tipping"
FeatureFlag.FrostedTipCard -> "Frosted Tip Card"
@@ -331,27 +189,13 @@ val FeatureFlag<*>.message: String
get() = when (this) {
FeatureFlag.CredentialManager -> "When enabled, you will gain the ability to utilize Google's Password Manager for storing and recovering access keys for easier login experience"
FeatureFlag.VibrateOnScan -> "When enabled, the device will vibrate once to indicate that the camera has registered the code on the bill"
- FeatureFlag.WelcomeBonusBill -> "When enabled, the welcome bonus after creating an account will be presented as a bill that will be placed in your wallet instead of simply toasting"
FeatureFlag.TransactionDetails -> "When enabled, you'll gain the ability to view details of each transaction from the balance screen"
- FeatureFlag.Pools -> "When enabled, you'll be able to participate in and create betting pools with other users for a chance to win a share of the prize"
- FeatureFlag.OnRamp -> "When enabled, you'll gain the ability to fund your wallet from external sources via providers using a debit card or via another wallet (like Phantom)"
- FeatureFlag.BillCustomizer -> "When enabled, you'll gain access to the bill customization playground"
- FeatureFlag.CashReservesEnabled -> "When enabled, USDC will be brandished as Cash Reserves throughout the app"
- FeatureFlag.MarketCapChart -> "When enabled, you'll gain access to the market cap chart in token info"
- FeatureFlag.CoinbaseOnRamp -> "When enabled, you'll gain access to the Coinbase onramp for token buys"
FeatureFlag.CoinbaseOnRampSandbox -> "When enabled, Coinbase onramp purchases will use the sandbox environment for testing"
- FeatureFlag.TokenDiscovery -> "When enabled, you'll gain access to leaderboards for tokens and discovery"
- FeatureFlag.CurrencyCreator -> "When enabled, you'll gain access to create new currencies"
FeatureFlag.BillTextures -> "When enabled, you'll gain the ability to select textures for bills during currency creation"
- FeatureFlag.DepositUsdc -> "When enabled, you'll gain the ability to deposit USDC directly from any external wallet app instead of purchasing a currency first and sell"
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.PhoneNumberSend -> "When enabled, you'll gain the ability to send cash directly to contacts via phone number and chat with them using the messenger"
- FeatureFlag.OnboardingPhoneVerification -> "When enabled, new accounts will be prompted to verify their phone number during onboarding"
- FeatureFlag.Messenger -> "When enabled, tapping a contact will open the chat messenger instead of navigating directly to send"
FeatureFlag.NavBar -> "Customize the order and labels of navigation bar buttons"
FeatureFlag.GiveUsdf -> "When enabled, you'll gain the ability to send USDF directly and give it as cash"
- FeatureFlag.AddMoneyUX -> "When enabled, the user experience for getting money into the app will be focused around 'Adding Money'"
FeatureFlag.ShowNetworkState -> "When enabled, you'll gain the ability to see the network state on the Scanner when offline"
FeatureFlag.Tipping -> "When enabled, you'll gain the ability to tip other users and set up your own tip card to receive tips"
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"
diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt
index 814751a646..641a127076 100644
--- a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt
+++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt
@@ -5,8 +5,6 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.tokens.FundingSource
import com.flipcash.app.core.tokens.SwapPurpose
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.funding.PaymentAction
import com.flipcash.app.funding.PurchaseMethod
import com.flipcash.app.funding.PurchaseMethodController
@@ -50,7 +48,6 @@ import kotlin.time.Duration.Companion.milliseconds
@Singleton
class InternalPurchaseMethodController @Inject constructor(
- features: FeatureFlagController,
private val userFlags: UserFlagsCoordinator,
reservesBalanceProvider: ReservesBalanceProvider,
exchange: Exchange,
@@ -70,14 +67,10 @@ class InternalPurchaseMethodController @Inject constructor(
private val _dismissals = MutableSharedFlow()
init {
- combine(
- features.observe(FeatureFlag.CoinbaseOnRamp),
- userFlags.resolvedFlags
- .map { it.supportedOnRampProviders.effectiveValue }
- .map { it.contains(OnRampProvider.Coinbase(OnRampType.Virtual)) }
- ) { enabled, available ->
- enabled && available
- }.onEach { coinbaseAvailable ->
+ userFlags.resolvedFlags
+ .map { it.supportedOnRampProviders.effectiveValue }
+ .map { it.contains(OnRampProvider.Coinbase(OnRampType.Virtual)) }
+ .onEach { coinbaseAvailable ->
_state.update { it.copy(coinbaseOnRampAvailable = coinbaseAvailable) }
}.launchIn(scope)
diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt
index e03c82350f..47df0c5151 100644
--- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt
+++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt
@@ -67,9 +67,7 @@ data class SessionState(
val contactDmUnreadCount: Int = 0,
val tipsUnreadCount: Int = 0,
val tokens: List = emptyList(),
- val isPhoneNumberSendEnabled: Boolean = false,
val isTippingEnabled: Boolean = false,
- val addMoneyUx: Boolean = false,
)
val LocalSessionController = staticCompositionLocalOf { null }
\ No newline at end of file
diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt
index a441c4face..6ae7a0532a 100644
--- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt
+++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt
@@ -273,13 +273,6 @@ class RealSessionController @Inject constructor(
stateHolder.update { it.copy(tokens = tokens) }
}.launchIn(scope)
- combine(
- featureFlagController.observe(FeatureFlag.PhoneNumberSend),
- userManager.state.map { it.flags?.enablePhoneNumberSend == true }
- ) { beta, server -> beta || server }
- .onEach { enabled -> stateHolder.update { it.copy(isPhoneNumberSendEnabled = enabled) } }
- .launchIn(scope)
-
featureFlagController.observe(FeatureFlag.Tipping)
.onEach { enabled -> stateHolder.update { it.copy(isTippingEnabled = enabled) } }
.launchIn(scope)
diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt
index 12db35e12f..2c83b7e7f2 100644
--- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt
+++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt
@@ -3,8 +3,6 @@ package com.flipcash.app.session.internal.delegates
import com.flipcash.app.analytics.Analytics
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.core.AppRoute
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.funding.PurchaseMethodController
import com.flipcash.app.session.DepositOperations
import com.flipcash.app.session.internal.SessionStateHolder
@@ -17,8 +15,6 @@ import com.getcode.manager.BottomBarManager
import com.getcode.util.resources.ResourceHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@@ -28,7 +24,6 @@ import javax.inject.Singleton
*
* This delegate owns the "getting money into the wallet" domain:
* - Presenting deposit/discovery options when the wallet is empty.
- * - Observing the `depositFirstUx` feature flag.
* - Executing and cancelling USDC deposit sweeps on lifecycle transitions.
*
* @see com.flipcash.app.session.internal.RealSessionController
@@ -42,43 +37,29 @@ class DepositDelegate @Inject constructor(
private val resources: ResourceHelper,
private val analytics: FlipcashAnalyticsService,
dispatchers: DispatcherProvider,
- featureFlagController: FeatureFlagController,
) : DepositOperations {
private val scope = CoroutineScope(dispatchers.IO + SupervisorJob())
- init {
- featureFlagController.observe(FeatureFlag.AddMoneyUX)
- .onEach { enabled -> stateHolder.update { it.copy(addMoneyUx = enabled) } }
- .launchIn(scope)
- }
-
override fun presentDepositOptions(onDismiss: (() -> Unit)?, onRoute: ((AppRoute) -> Unit)?) {
- // Prompt to add money only when the wallet is empty and add-money is available.
- // Otherwise the user has funds (e.g. reserves) but nothing giveable — or can't
- // add money at all — so steer them to discover/buy a currency.
- val addMoneyEnabled = stateHolder.current.addMoneyUx
+ // Prompt to add money only when the wallet is empty. Otherwise the user has funds
+ // (e.g. reserves) but nothing giveable, so steer them to discover/buy a currency.
val hasBalance = stateHolder.current.hasBalance
if (!hasBalance) {
- presentAddMoney(addMoneyEnabled, onRoute, onDismiss)
+ presentAddMoney(onRoute, onDismiss)
} else {
presentDiscoverCurrencies(onRoute, onDismiss)
}
}
private fun presentAddMoney(
- addMoneyEnabled: Boolean,
onRoute: ((AppRoute) -> Unit)?,
onDismiss: (() -> Unit)?
) {
BottomBarManager.showInfo(
title = resources.getString(R.string.title_noBalanceYet),
- message = if (addMoneyEnabled) {
- resources.getString(R.string.description_noBalanceYetToGive)
- } else {
- resources.getString(R.string.description_noBalanceYetDiscover)
- },
+ message = resources.getString(R.string.description_noBalanceYetToGive),
actions = listOf(
BottomBarAction(
text = resources.getString(R.string.action_addMoney)
diff --git a/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionStateHolderTest.kt b/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionStateHolderTest.kt
index a94ae59e9b..594b57d9ed 100644
--- a/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionStateHolderTest.kt
+++ b/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionStateHolderTest.kt
@@ -43,12 +43,11 @@ class SessionStateHolderTest {
@Test
fun `reset clears account-scoped state`() {
val holder = holder()
- holder.update { it.copy(hasGiveableBalance = true, contactDmUnreadCount = 3, isPhoneNumberSendEnabled = true) }
+ holder.update { it.copy(hasGiveableBalance = true, contactDmUnreadCount = 3) }
holder.reset()
val state = holder.state.value
assertEquals(false, state.hasGiveableBalance)
assertEquals(0, state.contactDmUnreadCount)
- assertEquals(false, state.isPhoneNumberSendEnabled)
}
@Test
diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
index 6158f1bcc5..329114fec3 100644
--- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
+++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
@@ -11,8 +11,6 @@ import com.flipcash.app.core.extensions.to
import com.flipcash.app.core.onramp.ui.buildPhantomButtonLabel
import com.flipcash.app.core.tokens.FundingSource
import com.flipcash.app.core.tokens.SwapPurpose
-import com.flipcash.app.featureflags.FeatureFlag
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.onramp.CoinbaseOnRampController
import com.flipcash.app.onramp.CoinbaseOnRampState
import com.flipcash.app.onramp.DeeplinkError
@@ -116,7 +114,6 @@ class SwapViewModel @Inject constructor(
private val coinbaseOnRampController: CoinbaseOnRampController,
private val phantomWalletController: PhantomWalletController,
private val userFlags: UserFlagsCoordinator,
- private val featureFlags: FeatureFlagController,
private val usdcDepositSweep: UsdcDepositSweep,
dispatchers: DispatcherProvider,
) : BaseViewModel(
@@ -171,8 +168,7 @@ class SwapViewModel @Inject constructor(
when (purpose) {
is SwapPurpose.Buy -> {
val limit = maxToAdd?.let { Fiat(it.first, it.second) }
- val mustAddMoney = featureFlags.get(FeatureFlag.AddMoneyUX)
- if (mustAddMoney && !stateFlow.value.isAddingMoney) {
+ if (!stateFlow.value.isAddingMoney) {
// tokenBalances are USD-denominated; convert to the user's preferred
// currency so the "Enter up to X" hint is localized and the over-max
// comparison (which relabels the entered amount with max.currencyCode)
@@ -442,12 +438,11 @@ class SwapViewModel @Inject constructor(
private suspend fun transactionLimit(): Fiat {
return when (stateFlow.value.purpose) {
is SwapPurpose.Buy -> {
- val mustAddMoney = featureFlags.observe(FeatureFlag.AddMoneyUX).value
val sendLimit = enteredAmount.currencyCode.let {
stateFlow.value.amountEntryState.limits?.sendLimitFor(it)
} ?: SendLimit.Zero
val maxSendPerDay = sendLimit.maxPerDay.toFiat(enteredAmount.currencyCode)
- if (mustAddMoney && !stateFlow.value.isAddingMoney) {
+ if (!stateFlow.value.isAddingMoney) {
val balances = tokenCoordinator.tokenBalances.firstOrNull().orEmpty().map { it.balance }
min((balances.maxOrNull() ?: Fiat.Zero), maxSendPerDay)
} else {
@@ -500,11 +495,10 @@ class SwapViewModel @Inject constructor(
val checkFundingAmount: suspend () -> Boolean = {
val limit = transactionLimit()
val isOverLimit = enteredAmount.valueGreaterThan(limit)
- val mustAddMoney = featureFlags.get(FeatureFlag.AddMoneyUX)
val isAddingMoney = stateFlow.value.isAddingMoney
if (isOverLimit) {
- if (mustAddMoney && !isAddingMoney) {
+ if (!isAddingMoney) {
BottomBarManager.showInfo(
title = resources.getString(R.string.title_insufficientBalance),
message = resources.getString(R.string.description_insufficientBalanceToUse),
@@ -652,7 +646,6 @@ class SwapViewModel @Inject constructor(
it to purpose
}
.onEach { (delegateState, purpose) ->
- val mustAddMoney = featureFlags.get(FeatureFlag.AddMoneyUX)
val isAddingMoney = stateFlow.value.isAddingMoney
addMoneyMethod?.let { method ->
analytics.addMoneyAmountConfirmed(
@@ -1237,9 +1230,6 @@ class SwapViewModel @Inject constructor(
.filterIsInstance()
.mapNotNull {
analytics.addMoneyOpened(Analytics.AddMoneySource.BuyShortfall)
- if (!featureFlags.get(FeatureFlag.AddMoneyUX)) {
- return@mapNotNull AppRoute.Transfers.Deposit(showOtherOptions = false)
- }
// present the add-money/deposit sheet; navigate to whatever the user picks.
// popToRoot = false: the chosen add-money route replaces this buy flow (see the
// OpenScreen handler in SwapEntryScreen), so finishing it pops a single level back
diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt
index fb82c06281..75b4c020de 100644
--- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt
+++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt
@@ -60,7 +60,6 @@ class TokenInfoViewModel @Inject constructor(
val mint: Mint? = null,
val token: Loadable = Loadable.Loading(),
val marketCap: Fiat? = null,
- val marketCapChartEnabled: Boolean = false,
val balance: LocalFiat = LocalFiat.Zero,
val showAppreciation: Boolean = false,
val showTransactionHistory: Boolean = false,
@@ -84,7 +83,6 @@ class TokenInfoViewModel @Inject constructor(
sealed interface Event {
data class CanGiveUsdf(val enabled: Boolean): Event
- data class MarketCapChartEnabled(val enabled: Boolean) : Event
data class OnMintProvided(val mint: Mint, val shortFall: Fiat? = null) : Event
data class OnTokenChanged(val token: Loadable, val shortFall: Fiat? = null) : Event
data class OnMarketCapChanged(val mcap: Fiat?) : Event
@@ -115,11 +113,6 @@ class TokenInfoViewModel @Inject constructor(
dispatchEvent(Event.CanGiveUsdf(it))
}.launchIn(viewModelScope)
- features.observe(FeatureFlag.MarketCapChart)
- .onEach {
- dispatchEvent(Event.MarketCapChartEnabled(it))
- }.launchIn(viewModelScope)
-
eventFlow
.filterIsInstance()
.onEach { dispatchEvent(Event.OnTokenChanged(Loadable.Loading())) }
@@ -289,9 +282,8 @@ class TokenInfoViewModel @Inject constructor(
val mint = stateFlow.value.mint ?: return@onEach
// A buy can be funded by USDF reserves or any other currency the user
// holds; only send them to deposit options first when they have nothing
- // to fund the swap with. This check is only done if AddMoneyUX is enabled.
- val addMoney = features.get(FeatureFlag.AddMoneyUX)
- if (!stateFlow.value.hasFundableBalance && addMoney) {
+ // to fund the swap with.
+ if (!stateFlow.value.hasFundableBalance) {
BottomBarManager.showInfo(
title = resources.getString(R.string.title_noBalanceYet),
message = resources.getString(R.string.description_noBalanceYetToBuy),
@@ -316,11 +308,6 @@ class TokenInfoViewModel @Inject constructor(
eventFlow
.filterIsInstance()
.mapNotNull {
- val depositFirstUx = features.get(FeatureFlag.AddMoneyUX)
- if (!depositFirstUx) {
- return@mapNotNull AppRoute.Transfers.Deposit(showOtherOptions = false)
- }
-
// popToRoot = false so finishing the deposit returns to this token
// info screen rather than dismissing the whole sheet.
purchaseMethodController.presentDepositOptions(popToRoot = false) }
@@ -338,7 +325,6 @@ class TokenInfoViewModel @Inject constructor(
companion object {
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
- is Event.MarketCapChartEnabled -> { state -> state.copy(marketCapChartEnabled = event.enabled) }
is Event.OnMintProvided -> { state -> state.copy(mint = event.mint) }
is Event.OnTokenChanged -> { state -> state.copy(token = event.token) }
is Event.OnMarketCapChanged -> { state -> state.copy(marketCap = event.mcap) }
diff --git a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt
index 093dee0284..a516f92acd 100644
--- a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt
+++ b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt
@@ -3,7 +3,6 @@ package com.flipcash.app.tokens.ui
import com.flipcash.app.activityfeed.ActivityFeedCoordinator
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.core.tokens.SwapPurpose
-import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.onramp.CoinbaseOnRampController
import com.flipcash.app.funding.PurchaseMethodController
import com.flipcash.app.tokens.TokenCoordinator
@@ -71,7 +70,6 @@ class SwapViewModelErrorTest {
private val coinbaseOnRampController = mockk(relaxed = true)
private val phantomWalletController = mockk(relaxed = true)
private val userFlagsCoordinator = mockk(relaxed = true)
- private val featureFlags = mockk(relaxed = true)
private val usdcDepositSweep = mockk(relaxed = true)
private val accountCluster = mockk(relaxed = true)
@@ -114,7 +112,6 @@ class SwapViewModelErrorTest {
phantomWalletController = phantomWalletController,
dispatchers = dispatchers,
userFlags = userFlagsCoordinator,
- featureFlags = featureFlags,
usdcDepositSweep = usdcDepositSweep,
)
}