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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,5 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp
## Git Conventions

- Conventional commits: `feat:`, `fix:`, `chore:`, with optional scope in parens (e.g., `feat(oc):`, `fix(tokens):`)
- Main branch: `main`
- Main branch: `code/cash`
- CI runs on all PRs (tests via Fastlane)
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 @@ -7,6 +7,8 @@ android {
}

dependencies {
testImplementation(kotlin("test"))

implementation(libs.compose.paging)

implementation(project(":apps:flipcash:shared:analytics"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.flipcash.app.balance.internal

import com.flipcash.services.internal.model.thirdparty.OnRampProvider
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class BalanceViewModelStateTest {

private val reduce = BalanceViewModel.Companion.updateStateForEvent

@Test
fun `default state has null provider`() {
assertNull(BalanceViewModel.State().preferredOnRampProvider)
}

@Test
fun `OnPreferredOnRampProviderChanged updates provider`() {
val provider = OnRampProvider.ManualDeposit
val updated = reduce(
BalanceViewModel.Event.OnPreferredOnRampProviderChanged(provider)
)(BalanceViewModel.State())
assertEquals(provider, updated.preferredOnRampProvider)
}

@Test
fun `OnPreferredOnRampProviderChanged with null clears provider`() {
val state = BalanceViewModel.State(
preferredOnRampProvider = OnRampProvider.ManualDeposit
)
val updated = reduce(
BalanceViewModel.Event.OnPreferredOnRampProviderChanged(null)
)(state)
assertNull(updated.preferredOnRampProvider)
}

@Test
fun `OpenCurrencySelection is no-op`() {
val state = BalanceViewModel.State(
preferredOnRampProvider = OnRampProvider.ManualDeposit
)
val updated = reduce(BalanceViewModel.Event.OpenCurrencySelection)(state)
assertEquals(state, updated)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.flipcash.app.cash.internal

import com.getcode.opencode.model.financial.Currency
import com.getcode.opencode.model.financial.CurrencyCode
import com.getcode.opencode.model.financial.Fiat
import com.getcode.solana.keys.Mint
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class CashScreenViewModelStateTest {

private val reduce = CashScreenViewModel.Companion.updateStateForEvent
private fun mint() = Mint(ByteArray(32) { 1 }.toList())

// --- Default state ---

@Test
fun `default state has null token and address`() {
val state = CashScreenViewModel.State()
assertNull(state.selectedTokenAddress)
assertNull(state.token)
assertNull(state.limits)
assertNull(state.maxForGive)
assertFalse(state.canGive)
}

// --- State reducers ---

@Test
fun `OnTokenSelected sets selectedTokenAddress`() {
val mint = mint()
val updated = reduce(CashScreenViewModel.Event.OnTokenSelected(mint))(CashScreenViewModel.State())
assertEquals(mint, updated.selectedTokenAddress)
}

@Test
fun `OnAmountChanged updates amountAnimatedModel`() {
val model = AmountAnimatedInputUiModel(lastPressedBackspace = true)
val updated = reduce(CashScreenViewModel.Event.OnAmountChanged(model))(CashScreenViewModel.State())
assertEquals(model, updated.amountAnimatedModel)
}

@Test
fun `UpdateLoadingState sets loading`() {
val updated = reduce(
CashScreenViewModel.Event.UpdateLoadingState(loading = true)
)(CashScreenViewModel.State())
assertTrue(updated.generatingBill.loading)
assertFalse(updated.generatingBill.success)
}

@Test
fun `UpdateLoadingState sets success`() {
val updated = reduce(
CashScreenViewModel.Event.UpdateLoadingState(loading = false, success = true)
)(CashScreenViewModel.State())
assertTrue(updated.generatingBill.success)
assertFalse(updated.generatingBill.loading)
}

@Test
fun `OnMaxDetermined sets maxForGive`() {
val updated = reduce(
CashScreenViewModel.Event.OnMaxDetermined(max = 50.0, currencyCode = CurrencyCode.USD)
)(CashScreenViewModel.State())
assertEquals(50.0 to CurrencyCode.USD, updated.maxForGive)
}

@Test
fun `OnLimitsChanged sets limits`() {
val updated = reduce(
CashScreenViewModel.Event.OnLimitsChanged(null)
)(CashScreenViewModel.State())
assertNull(updated.limits)
}

@Test
fun `OnCurrencyChanged sets currencyModel`() {
val currency = Currency(code = "EUR", name = "Euro", symbol = "€", rate = 0.92)
val updated = reduce(
CashScreenViewModel.Event.OnCurrencyChanged(currency)
)(CashScreenViewModel.State())
assertNotNull(updated.currencyModel.selected)
assertEquals("EUR", updated.currencyModel.selected?.code)
assertEquals(CurrencyCode.EUR, updated.currencyModel.code)
}

@Test
fun `OnCurrencyChanged preserves fractionUnits`() {
val currency = Currency(code = "JPY", name = "Japanese Yen", symbol = "¥", fractionUnits = 0)
val updated = reduce(
CashScreenViewModel.Event.OnCurrencyChanged(currency)
)(CashScreenViewModel.State())
assertEquals(0, updated.currencyModel.fractionUnits)
}

// --- No-op events ---

@Test
fun `no-op events return state unchanged`() {
val state = CashScreenViewModel.State(selectedTokenAddress = mint())
val noOpEvents = listOf(
CashScreenViewModel.Event.InitializeToken(null),
CashScreenViewModel.Event.OnBackspace,
CashScreenViewModel.Event.OnGive,
CashScreenViewModel.Event.OnEnteredNumberChanged(),
CashScreenViewModel.Event.OnNumberPressed(5),
CashScreenViewModel.Event.OnDecimalPressed,
CashScreenViewModel.Event.AddCashToWallet(Fiat.Zero),
CashScreenViewModel.Event.OpenScreen(com.flipcash.app.core.AppRoute.Loading),
)
noOpEvents.forEach { event ->
assertEquals(state, reduce(event)(state), "Event $event should be no-op")
}
}

// --- Computed: canGive ---

@Test
fun `canGive is false when amount is zero`() {
assertFalse(CashScreenViewModel.State().canGive)
}

// --- Computed: maxAvailableForGive ---

@Test
fun `maxAvailableForGive is empty when maxForGive is null`() {
val state = CashScreenViewModel.State(maxForGive = null)
assertEquals("", state.maxAvailableForGive)
}

@Test
fun `maxAvailableForGive is formatted when maxForGive is set`() {
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
assertTrue(state.maxAvailableForGive.isNotEmpty())
}

// --- Computed: isError ---

@Test
fun `isError is true when maxForGive is null`() {
// Default amountData has "0" text (not empty), and maxForGive is null → error
assertTrue(CashScreenViewModel.State().isError)
}

@Test
fun `isError is false when amount within maxForGive`() {
// Default amount 0.0 <= 100.0
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
assertFalse(state.isError)
}
}
2 changes: 2 additions & 0 deletions apps/flipcash/features/deposit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ android {
}

dependencies {
testImplementation(kotlin("test"))

implementation(project(":libs:messaging"))

implementation(project(":apps:flipcash:shared:featureflags"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.flipcash.app.deposit.internal

import com.getcode.solana.keys.PublicKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

class DepositViewModelStateTest {

private val reduce = DepositViewModel.Companion.updateStateForEvent
private fun mint() = com.getcode.solana.keys.Mint(ByteArray(32) { 1 }.toList())

@Test
fun `default state has null address and token name`() {
val state = DepositViewModel.State()
assertNull(state.selectedTokenAddress)
assertNull(state.tokenName)
assertEquals("", state.depositAddress)
assertFalse(state.isCopied)
}

@Test
fun `OnMintSelected sets selected token address`() {
val mint = mint()
val updated = reduce(DepositViewModel.Event.OnMintSelected(mint))(DepositViewModel.State())
assertEquals(mint, updated.selectedTokenAddress)
}

@Test
fun `OnTokenChanged sets address and name`() {
val updated = reduce(
DepositViewModel.Event.OnTokenChanged(address = "abc123", name = "TestToken")
)(DepositViewModel.State())
assertEquals("abc123", updated.depositAddress)
assertEquals("TestToken", updated.tokenName)
}

@Test
fun `SetCopied true then false roundtrip`() {
val state = DepositViewModel.State()
val copied = reduce(DepositViewModel.Event.SetCopied(true))(state)
assertTrue(copied.isCopied)
val uncopied = reduce(DepositViewModel.Event.SetCopied(false))(copied)
assertFalse(uncopied.isCopied)
}

@Test
fun `CopyAddress is no-op`() {
val state = DepositViewModel.State(depositAddress = "addr", tokenName = "T")
val updated = reduce(DepositViewModel.Event.CopyAddress)(state)
assertEquals(state, updated)
}

@Test
fun `Exit is no-op`() {
val state = DepositViewModel.State(depositAddress = "addr")
val updated = reduce(DepositViewModel.Event.Exit)(state)
assertEquals(state, updated)
}
}
2 changes: 2 additions & 0 deletions apps/flipcash/features/myaccount/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ android {
}

dependencies {
testImplementation(kotlin("test"))

implementation(project(":apps:flipcash:shared:authentication"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:menu"))
Expand Down
Loading
Loading