Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,6 @@
<string name="description_noBalanceYetToSend">Add money to send cash</string>
<string name="description_noBalanceYetForBalance">Add money to get started</string>
<string name="description_noBalanceYetToCreate">Add money to create a currency</string>
<string name="description_noBalanceYetDiscover">Buy your first currency to get started</string>
<string name="description_noBalanceYetToTip">Add money to send tips</string>
<string name="action_dismiss">Dismiss</string>
<string name="title_success">Success</string>
Expand Down
2 changes: 2 additions & 0 deletions apps/flipcash/features/balance/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ android {

dependencies {
testImplementation(kotlin("test"))
testImplementation(libs.bundles.unit.testing)
testImplementation(libs.bundles.compose.ui.testing)

implementation(libs.compose.paging)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
}
Expand All @@ -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
)
}
)
}
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,20 +27,17 @@ internal class BalanceViewModel @Inject constructor(
userFlags: UserFlagsCoordinator,
dispatchers: DispatcherProvider,
purchaseMethodController: PurchaseMethodController,
featureFlags: FeatureFlagController,
analytics: FlipcashAnalyticsService,
) : BaseViewModel<BalanceViewModel.State, BalanceViewModel.Event>(
initialState = State(),
updateStateForEvent = updateStateForEvent,
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
Expand All @@ -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 }
Expand All @@ -65,12 +56,7 @@ internal class BalanceViewModel @Inject constructor(

eventFlow
.filterIsInstance<Event.PresentDepositOptions>()
.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)) }
Expand All @@ -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) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<AppRoute>(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<OnRampProvider.Defined>()
val updated = BalanceViewModel.updateStateForEvent(
BalanceViewModel.Event.OnPreferredOnRampProviderChanged(provider)
)(BalanceViewModel.State())
assertEquals(provider, updated.preferredOnRampProvider)
}
}
Loading
Loading