From 7c40e2a28db6c632040455249c2bd5b6848588d6 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 11:51:28 -0400 Subject: [PATCH 1/4] refactor(featureflags): inline launched beta flags at call sites The following flags shipped (launched = true) and already returned true unconditionally at runtime, so inline them as always-on and delete the now-dead disabled branches: WelcomeBonusBill, Pools, OnRamp, BillCustomizer, CurrencyCreator, CashReserves, MarketCapChart, CoinbaseOnRamp, TokenDiscovery, DepositUsdc, PhoneNumberSend, OnboardingPhoneVerification, Messenger, AddMoneyUX. - Collapse constant flag state into defaults and remove the observe/event/reducer plumbing (e.g. depositFirstUx, marketCapChartEnabled, createEnabled, isPhoneNumberSendEnabled, addMoneyUx) and propagate the always-on value into UI. - Drop the dead server-flag OR (flag || enablePhoneNumberSend) that was always true, and remove RealChatCoordinator's now-redundant flag observer / isChatEnabled. - Remove FeatureFlagController dependencies left unused after inlining, plus the orphaned description_noBalanceYetDiscover string. Behavior-preserving: each flag already evaluated to true for all users. --- .../core/src/main/res/values/strings.xml | 1 - .../balance/internal/BalanceScreenContent.kt | 33 +---- .../app/balance/internal/BalanceViewModel.kt | 17 +-- .../internal/CurrencyCreatorViewModel.kt | 81 ++++++------- .../directsend/internal/SendFlowViewModel.kt | 8 +- .../internal/TokenDiscoveryScreenContent.kt | 1 - .../internal/TokenDiscoveryViewModel.kt | 13 -- .../internal/components/TokenLeaderboard.kt | 5 +- .../app/login/OnboardingFlowScreen.kt | 20 ++- .../app/login/router/LoginViewModel.kt | 19 +-- .../app/menu/internal/MenuScreenContent.kt | 12 +- .../app/menu/internal/MenuScreenViewModel.kt | 8 -- .../app/messenger/internal/ChatViewModel.kt | 25 +--- .../ui/components/ScannerNavigationBar.kt | 3 +- .../app/tokens/internal/TokenInfoScreen.kt | 1 - .../components/info/MarketCapSection.kt | 114 +++++++++--------- .../com/flipcash/app/auth/AuthManager.kt | 6 +- .../chat/internal/RealChatCoordinator.kt | 42 +------ .../app/contacts/ContactCoordinator.kt | 8 -- .../InternalPurchaseMethodController.kt | 15 +-- .../flipcash/app/session/SessionController.kt | 2 - .../session/internal/RealSessionController.kt | 7 -- .../internal/delegates/DepositDelegate.kt | 27 +---- .../internal/SessionStateHolderTest.kt | 3 +- .../flipcash/app/tokens/ui/SwapViewModel.kt | 16 +-- .../app/tokens/ui/TokenInfoViewModel.kt | 18 +-- 26 files changed, 140 insertions(+), 365 deletions(-) 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/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..82a91f2f3f 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,7 +48,6 @@ internal fun BalanceScreen( val balanceState by viewModel.stateFlow.collectAsStateWithLifecycle() val tokenState by tokenViewModel.stateFlow.collectAsStateWithLifecycle() BalanceScreenContent( - addMoneyUx = balanceState.depositFirstUx, tokenState = tokenState, dispatchEvent = viewModel::dispatchEvent ) @@ -56,7 +55,6 @@ internal fun BalanceScreen( @Composable private fun BalanceScreenContent( - addMoneyUx: Boolean = false, 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/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/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/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/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) } From d094a38fa819c63d12516da0084587f372631e16 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 11:51:35 -0400 Subject: [PATCH 2/4] chore(featureflags): remove launched beta flag definitions Delete the 14 shipped (launched = true) flag data objects and their title/message entries now that all call sites treat them as always-on. Remaining flags are the genuinely in-flight ones (CredentialManager, VibrateOnScan, TransactionDetails, CoinbaseOnRampSandbox, BillTextures, BackgroundReset, ContactPickerMode, GiveUsdf, NavBar, ShowNetworkState, Tipping, FrostedTipCard, Blocklist). --- .../flipcash/app/featureflags/FeatureFlag.kt | 156 ------------------ 1 file changed, 156 deletions(-) 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" From b6f8ba599aca2dffd17d91a950dd5d9bb576558f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 12:43:37 -0400 Subject: [PATCH 3/4] test: drop removed featureFlags arg from existing viewmodel tests LoginViewModel, SwapViewModel and RealChatCoordinator no longer take a FeatureFlagController after the launched-flag inlining, so their existing tests no longer compile. Remove the stale constructor argument. --- .../app/login/router/LoginViewModelCreateAccountTest.kt | 3 --- .../com/flipcash/app/login/router/LoginViewModelErrorTest.kt | 3 --- .../flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt | 2 -- .../com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt | 2 -- .../com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt | 3 --- 5 files changed, 13 deletions(-) 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/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/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, ) } From ec4ff06b3d172ce4721f31844ba195da9089470f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 12:43:38 -0400 Subject: [PATCH 4/4] test: add regression coverage for launched beta flag removal Lock in the always-on behaviour left behind by the removed flags: - BalanceViewModelTest: PresentDepositOptions always routes through the add-money deposit sheet (AddMoneyUX). - BalanceScreenContentTest: empty wallet shows the "Add Money" CTA and never the old "Discover Currencies" fallback; tapping it opens deposit options. - LoginViewModelPhoneVerificationTest: phone verification is gated only by whether a verified phone is linked (OnboardingPhoneVerification/PhoneNumberSend). Expose BalanceScreenContent as internal for the Robolectric UI test, matching the existing *ScreenContentTest pattern. --- .../features/balance/build.gradle.kts | 2 + .../balance/internal/BalanceScreenContent.kt | 2 +- .../internal/BalanceScreenContentTest.kt | 76 ++++++++++++ .../balance/internal/BalanceViewModelTest.kt | 76 ++++++++++++ .../LoginViewModelPhoneVerificationTest.kt | 108 ++++++++++++++++++ 5 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceScreenContentTest.kt create mode 100644 apps/flipcash/features/balance/src/test/kotlin/com/flipcash/app/balance/internal/BalanceViewModelTest.kt create mode 100644 apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelPhoneVerificationTest.kt 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 82a91f2f3f..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 @@ -54,7 +54,7 @@ internal fun BalanceScreen( } @Composable -private fun BalanceScreenContent( +internal fun BalanceScreenContent( tokenState: SelectTokenViewModel.State, dispatchEvent: (BalanceViewModel.Event) -> Unit ) { 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/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) + } +}