Skip to content

Commit b6e1aa2

Browse files
authored
refactor(scannable): introduce Scannable hierarchy replacing Bill (#1116)
* feat(scannable): add Scannable hierarchy, forToken factory, stamped * fix(scannable): named args in forToken factory, stronger field-passthrough assertions - Convert positional GoldBar/CashBill constructor calls in forToken to named arguments to prevent silent field-order drift - Extend forToken passthrough test to assert every forwarded field (disableGestures, confirmationDelay, data, nonce) with non-default values - Assert bar.token survives stamped copy in GoldBar stamped test * refactor(scannable): migrate Bill consumers to Scannable.Payable Replaces the single-variant Bill sealed interface with Scannable.Payable (CashBill/GoldBar). renderAsBill removed; USDF->GoldBar decided by the forToken factory. presentBillToUser uses stamped(); customization guards narrow to Scannable.CashBill. Signed-off-by: Brandon McAnsh <brandon@bmcreations.dev> * refactor(scannable): drop dead customization guards, tighten airdrop test, doc/comment cleanup --------- Signed-off-by: Brandon McAnsh <brandon@bmcreations.dev>
1 parent bec705d commit b6e1aa2

25 files changed

Lines changed: 329 additions & 210 deletions

File tree

‎apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt‎

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,13 @@ import androidx.compose.ui.graphics.painter.Painter
55
import androidx.compose.ui.res.painterResource
66
import androidx.compose.ui.res.stringResource
77
import com.flipcash.core.R
8-
import com.getcode.opencode.internal.manager.VerifiedState
98
import com.getcode.opencode.model.financial.Fiat
109
import com.getcode.opencode.model.financial.LocalFiat
1110
import com.getcode.opencode.model.financial.Token
12-
import com.getcode.opencode.model.financial.usdf
13-
import com.getcode.solana.keys.Mint
1411
import kotlin.time.Duration
1512

1613
data class BillState(
17-
val bill: Bill?,
14+
val bill: Scannable.Payable?,
1815
val showToast: Boolean,
1916
val toast: BillToast?,
2017
val valuation: Valuation?,
@@ -86,51 +83,6 @@ data class BillState(
8683
}
8784
}
8885

89-
sealed interface Bill {
90-
val didReceive: Boolean
91-
val confirmationDelay: Duration
92-
val token: Token
93-
val amount: LocalFiat
94-
val data: List<Byte>
95-
val disableGestures: Boolean
96-
97-
enum class Kind {
98-
cash, airdrop
99-
}
100-
101-
val canSwipeToDismiss: Boolean
102-
get() = when (this) {
103-
is Cash -> !disableGestures
104-
}
105-
106-
val canFlip: Boolean
107-
108-
val metadata: Metadata
109-
get() {
110-
return when (this) {
111-
is Cash -> Metadata(
112-
token = token,
113-
amount = amount,
114-
data = data
115-
)
116-
}
117-
}
118-
119-
data class Cash(
120-
override val token: Token,
121-
override val amount: LocalFiat,
122-
override val didReceive: Boolean = false,
123-
override val disableGestures: Boolean = false,
124-
override val confirmationDelay: Duration = Duration.ZERO,
125-
override val data: List<Byte> = emptyList(),
126-
val kind: Kind = Kind.cash,
127-
val verifiedState: VerifiedState? = null,
128-
val nonce: List<Byte> = emptyList(),
129-
val renderAsBill: Boolean = token.address != Mint.usdf,
130-
) : Bill {
131-
override val canFlip: Boolean = false
132-
}
133-
}
13486

13587
sealed interface Valuation
13688
data class PaymentValuation(val amount: Fiat): Valuation
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.flipcash.app.core.bill
2+
3+
import com.getcode.opencode.internal.manager.VerifiedState
4+
import com.getcode.opencode.model.financial.LocalFiat
5+
import com.getcode.opencode.model.financial.Token
6+
import com.getcode.solana.keys.Mint
7+
import kotlin.time.Duration
8+
9+
/**
10+
* Anything that renders a scannable code. The single common member is [data] —
11+
* the encoded KikCode bytes (an `OpenCodePayload.codeData`).
12+
*/
13+
sealed interface Scannable {
14+
val data: List<Byte>
15+
16+
/**
17+
* A payment-bearing scannable (carries a token + amount). [CashBill] and [GoldBar]
18+
* differ only in which renderer draws them; the type IS the render choice.
19+
*/
20+
sealed interface Payable : Scannable {
21+
val token: Token
22+
val amount: LocalFiat
23+
val didReceive: Boolean
24+
val disableGestures: Boolean
25+
val confirmationDelay: Duration
26+
val kind: Kind
27+
val verifiedState: VerifiedState?
28+
val nonce: List<Byte>
29+
30+
enum class Kind { cash, airdrop }
31+
32+
val canSwipeToDismiss: Boolean get() = !disableGestures
33+
val canFlip: Boolean get() = false
34+
val metadata: Metadata get() = Metadata(token = token, amount = amount, data = data)
35+
36+
/** Returns a copy of this bill with its scannable [code] (+ [nonce]) stamped in. */
37+
fun stamped(code: List<Byte>, nonce: List<Byte>): Payable
38+
39+
companion object {
40+
/** Applies the USDF->[GoldBar] rule; every other token -> [CashBill]. */
41+
fun forToken(
42+
token: Token,
43+
amount: LocalFiat,
44+
didReceive: Boolean = false,
45+
disableGestures: Boolean = false,
46+
confirmationDelay: Duration = Duration.ZERO,
47+
data: List<Byte> = emptyList(),
48+
kind: Kind = Kind.cash,
49+
verifiedState: VerifiedState? = null,
50+
nonce: List<Byte> = emptyList(),
51+
): Payable = if (token.address == Mint.usdf) {
52+
GoldBar(
53+
token = token, amount = amount, didReceive = didReceive,
54+
disableGestures = disableGestures, confirmationDelay = confirmationDelay,
55+
data = data, kind = kind, verifiedState = verifiedState, nonce = nonce,
56+
)
57+
} else {
58+
CashBill(
59+
token = token, amount = amount, didReceive = didReceive,
60+
disableGestures = disableGestures, confirmationDelay = confirmationDelay,
61+
data = data, kind = kind, verifiedState = verifiedState, nonce = nonce,
62+
)
63+
}
64+
}
65+
}
66+
67+
data class CashBill(
68+
override val token: Token,
69+
override val amount: LocalFiat,
70+
override val didReceive: Boolean = false,
71+
override val disableGestures: Boolean = false,
72+
override val confirmationDelay: Duration = Duration.ZERO,
73+
override val data: List<Byte> = emptyList(),
74+
override val kind: Payable.Kind = Payable.Kind.cash,
75+
override val verifiedState: VerifiedState? = null,
76+
override val nonce: List<Byte> = emptyList(),
77+
) : Payable {
78+
override fun stamped(code: List<Byte>, nonce: List<Byte>) = copy(data = code, nonce = nonce)
79+
}
80+
81+
data class GoldBar(
82+
override val token: Token,
83+
override val amount: LocalFiat,
84+
override val didReceive: Boolean = false,
85+
override val disableGestures: Boolean = false,
86+
override val confirmationDelay: Duration = Duration.ZERO,
87+
override val data: List<Byte> = emptyList(),
88+
override val kind: Payable.Kind = Payable.Kind.cash,
89+
override val verifiedState: VerifiedState? = null,
90+
override val nonce: List<Byte> = emptyList(),
91+
) : Payable {
92+
override fun stamped(code: List<Byte>, nonce: List<Byte>) = copy(data = code, nonce = nonce)
93+
}
94+
95+
data class TipCard(
96+
override val data: List<Byte>,
97+
val username: String,
98+
) : Scannable
99+
}

‎apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/bill/BillStateTest.kt‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class BillStateTest {
6565

6666
@Test
6767
fun `canSwipeToDismiss returns true when bill allows gestures`() {
68-
val bill = Bill.Cash(
68+
val bill = Scannable.CashBill(
6969
token = testToken(),
7070
amount = testLocalFiat(),
7171
disableGestures = false,
@@ -77,7 +77,7 @@ class BillStateTest {
7777

7878
@Test
7979
fun `canSwipeToDismiss returns false when bill disableGestures is true`() {
80-
val bill = Bill.Cash(
80+
val bill = Scannable.CashBill(
8181
token = testToken(),
8282
amount = testLocalFiat(),
8383
disableGestures = true,
@@ -100,7 +100,7 @@ class BillStateTest {
100100

101101
@Test
102102
fun `confirmationDelayMillis converts Duration correctly`() {
103-
val bill = Bill.Cash(
103+
val bill = Scannable.CashBill(
104104
token = testToken(),
105105
amount = testLocalFiat(),
106106
confirmationDelay = 3.seconds,
@@ -119,15 +119,15 @@ class BillStateTest {
119119

120120
// endregion
121121

122-
// region Bill.Cash metadata and canFlip
122+
// region Scannable.CashBill metadata and canFlip
123123

124124
@Test
125125
fun `Bill Cash metadata extracts token and amount`() {
126126
val token = testToken()
127127
val amount = testLocalFiat()
128128
val data = listOf<Byte>(1, 2, 3)
129129

130-
val bill = Bill.Cash(
130+
val bill = Scannable.CashBill(
131131
token = token,
132132
amount = amount,
133133
data = data,
@@ -142,7 +142,7 @@ class BillStateTest {
142142

143143
@Test
144144
fun `Bill Cash canFlip is always false`() {
145-
val bill = Bill.Cash(
145+
val bill = Scannable.CashBill(
146146
token = testToken(),
147147
amount = testLocalFiat(),
148148
)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.flipcash.app.core.bill
2+
3+
import com.getcode.opencode.model.financial.Fiat
4+
import com.getcode.opencode.model.financial.HolderMetrics
5+
import com.getcode.opencode.model.financial.LocalFiat
6+
import com.getcode.opencode.model.financial.MintMetadata
7+
import com.getcode.opencode.model.financial.Rate
8+
import com.getcode.opencode.model.financial.Token
9+
import com.getcode.opencode.model.financial.VmMetadata
10+
import com.getcode.solana.keys.Mint
11+
import com.getcode.solana.keys.PublicKey
12+
import kotlin.test.Test
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertIs
15+
import kotlin.test.assertTrue
16+
import kotlin.time.Duration.Companion.seconds
17+
18+
class ScannableTest {
19+
20+
private fun tokenWith(mint: Mint): Token = MintMetadata(
21+
address = mint,
22+
decimals = 6,
23+
name = "Test",
24+
symbol = "TST",
25+
createdAt = null,
26+
description = "",
27+
imageUrl = "",
28+
vmMetadata = VmMetadata(
29+
vm = PublicKey.fromBase58("11111111111111111111111111111111"),
30+
authority = PublicKey.fromBase58("11111111111111111111111111111111"),
31+
lockDurationInDays = 21
32+
),
33+
launchpadMetadata = null,
34+
billCustomizations = null,
35+
socialLinks = emptyList(),
36+
holderMetrics = HolderMetrics.None,
37+
)
38+
39+
private fun localFiat(): LocalFiat = LocalFiat(
40+
underlyingTokenAmount = Fiat(5.0),
41+
nativeAmount = Fiat(5.0),
42+
rate = Rate.oneToOne,
43+
mint = Mint.usdf,
44+
)
45+
46+
private val nonUsdfMint = Mint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") // USDC — not USDF
47+
48+
@Test
49+
fun `forToken returns GoldBar for usdf mint`() {
50+
val bill = Scannable.Payable.forToken(token = tokenWith(Mint.usdf), amount = localFiat())
51+
assertIs<Scannable.GoldBar>(bill)
52+
}
53+
54+
@Test
55+
fun `forToken returns CashBill for non-usdf mint`() {
56+
val bill = Scannable.Payable.forToken(token = tokenWith(nonUsdfMint), amount = localFiat())
57+
assertIs<Scannable.CashBill>(bill)
58+
}
59+
60+
@Test
61+
fun `forToken preserves passed-through fields`() {
62+
val expectedData = listOf<Byte>(1, 2, 3)
63+
val expectedNonce = listOf<Byte>(9)
64+
val expectedDelay = 4.seconds
65+
val bill = Scannable.Payable.forToken(
66+
token = tokenWith(nonUsdfMint),
67+
amount = localFiat(),
68+
didReceive = true,
69+
disableGestures = true,
70+
confirmationDelay = expectedDelay,
71+
data = expectedData,
72+
kind = Scannable.Payable.Kind.airdrop,
73+
nonce = expectedNonce,
74+
)
75+
assertTrue(bill.didReceive)
76+
assertTrue(bill.disableGestures)
77+
assertEquals(expectedDelay, bill.confirmationDelay)
78+
assertEquals(expectedData, bill.data)
79+
assertEquals(Scannable.Payable.Kind.airdrop, bill.kind)
80+
assertEquals(expectedNonce, bill.nonce)
81+
}
82+
83+
@Test
84+
fun `stamped replaces code and nonce on CashBill and keeps its type`() {
85+
val bill = Scannable.CashBill(token = tokenWith(nonUsdfMint), amount = localFiat())
86+
val code = listOf<Byte>(1, 2, 3)
87+
val nonce = listOf<Byte>(9, 8)
88+
val stamped = bill.stamped(code = code, nonce = nonce)
89+
assertIs<Scannable.CashBill>(stamped)
90+
assertEquals(code, stamped.data)
91+
assertEquals(nonce, stamped.nonce)
92+
assertEquals(bill.token, stamped.token)
93+
}
94+
95+
@Test
96+
fun `stamped keeps GoldBar type`() {
97+
val bar = Scannable.GoldBar(token = tokenWith(Mint.usdf), amount = localFiat())
98+
val stamped = bar.stamped(code = listOf(7), nonce = emptyList())
99+
assertIs<Scannable.GoldBar>(stamped)
100+
assertEquals(listOf<Byte>(7), stamped.data)
101+
assertEquals(bar.token, stamped.token)
102+
}
103+
}

‎apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import androidx.compose.ui.unit.dp
4141
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4242
import com.flipcash.app.bill.customization.components.BillPlayground
4343
import com.flipcash.app.bills.AnimatedBill
44-
import com.flipcash.app.core.bill.Bill
44+
import com.flipcash.app.core.bill.Scannable
4545
import com.flipcash.features.bill.playground.R
4646
import com.getcode.theme.CodeTheme
4747
import com.getcode.ui.components.AppBarDefaults
@@ -82,7 +82,6 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
8282
derivedStateOf {
8383
if (!playgroundState.context.renderAsOverlay) return@derivedStateOf null
8484
val bill = playgroundState.bill ?: return@derivedStateOf null
85-
if (bill !is Bill.Cash) return@derivedStateOf null
8685
bill.copy(
8786
token = bill.token.copy(
8887
billCustomizations = customizationsOptions

‎apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import androidx.lifecycle.viewModelScope
44
import com.flipcash.app.analytics.Analytics
55
import com.flipcash.app.analytics.FlipcashAnalyticsService
66
import com.flipcash.app.core.AppRoute
7-
import com.flipcash.app.core.bill.Bill
7+
import com.flipcash.app.core.bill.Scannable
88
import com.flipcash.app.core.tokens.SwapPurpose
99
import com.flipcash.app.core.ui.CurrencyHolder
1010
import com.flipcash.app.tokens.TokenCoordinator
@@ -105,7 +105,7 @@ internal class CashScreenViewModel @Inject constructor(
105105
data class OnCurrencyChanged(val model: com.getcode.opencode.model.financial.Currency) : Event
106106
data class OnLimitsChanged(val limits: Limits?) : Event
107107
data object OnGive : Event
108-
data class PresentBill(val bill: Bill.Cash) : Event
108+
data class PresentBill(val bill: Scannable.Payable) : Event
109109

110110
data class AddCashToWallet(val amount: Fiat) : Event
111111
data class UpdateLoadingState(val loading: Boolean = false, val success: Boolean = false) :
@@ -260,7 +260,7 @@ internal class CashScreenViewModel @Inject constructor(
260260
return@onEach
261261
}
262262

263-
val bill = Bill.Cash(
263+
val bill = Scannable.Payable.forToken(
264264
token = stateFlow.value.token!!.token,
265265
amount = result.localFiat,
266266
verifiedState = result.verifiedState,

0 commit comments

Comments
 (0)