Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.flipcash.core.R
import com.getcode.opencode.internal.manager.VerifiedState
import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.usdf
import com.getcode.solana.keys.Mint
import kotlin.time.Duration

data class BillState(
val bill: Bill?,
val bill: Scannable.Payable?,
val showToast: Boolean,
val toast: BillToast?,
val valuation: Valuation?,
Expand Down Expand Up @@ -86,51 +83,6 @@ data class BillState(
}
}

sealed interface Bill {
val didReceive: Boolean
val confirmationDelay: Duration
val token: Token
val amount: LocalFiat
val data: List<Byte>
val disableGestures: Boolean

enum class Kind {
cash, airdrop
}

val canSwipeToDismiss: Boolean
get() = when (this) {
is Cash -> !disableGestures
}

val canFlip: Boolean

val metadata: Metadata
get() {
return when (this) {
is Cash -> Metadata(
token = token,
amount = amount,
data = data
)
}
}

data class Cash(
override val token: Token,
override val amount: LocalFiat,
override val didReceive: Boolean = false,
override val disableGestures: Boolean = false,
override val confirmationDelay: Duration = Duration.ZERO,
override val data: List<Byte> = emptyList(),
val kind: Kind = Kind.cash,
val verifiedState: VerifiedState? = null,
val nonce: List<Byte> = emptyList(),
val renderAsBill: Boolean = token.address != Mint.usdf,
) : Bill {
override val canFlip: Boolean = false
}
}

sealed interface Valuation
data class PaymentValuation(val amount: Fiat): Valuation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.flipcash.app.core.bill

import com.getcode.opencode.internal.manager.VerifiedState
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Token
import com.getcode.solana.keys.Mint
import kotlin.time.Duration

/**
* Anything that renders a scannable code. The single common member is [data] —
* the encoded KikCode bytes (an `OpenCodePayload.codeData`).
*/
sealed interface Scannable {
val data: List<Byte>

/**
* A payment-bearing scannable (carries a token + amount). [CashBill] and [GoldBar]
* differ only in which renderer draws them; the type IS the render choice.
*/
sealed interface Payable : Scannable {
val token: Token
val amount: LocalFiat
val didReceive: Boolean
val disableGestures: Boolean
val confirmationDelay: Duration
val kind: Kind
val verifiedState: VerifiedState?
val nonce: List<Byte>

enum class Kind { cash, airdrop }

val canSwipeToDismiss: Boolean get() = !disableGestures
val canFlip: Boolean get() = false
val metadata: Metadata get() = Metadata(token = token, amount = amount, data = data)

/** Returns a copy of this bill with its scannable [code] (+ [nonce]) stamped in. */
fun stamped(code: List<Byte>, nonce: List<Byte>): Payable

companion object {
/** Applies the USDF->[GoldBar] rule; every other token -> [CashBill]. */
fun forToken(
token: Token,
amount: LocalFiat,
didReceive: Boolean = false,
disableGestures: Boolean = false,
confirmationDelay: Duration = Duration.ZERO,
data: List<Byte> = emptyList(),
kind: Kind = Kind.cash,
verifiedState: VerifiedState? = null,
nonce: List<Byte> = emptyList(),
): Payable = if (token.address == Mint.usdf) {
GoldBar(
token = token, amount = amount, didReceive = didReceive,
disableGestures = disableGestures, confirmationDelay = confirmationDelay,
data = data, kind = kind, verifiedState = verifiedState, nonce = nonce,
)
} else {
CashBill(
token = token, amount = amount, didReceive = didReceive,
disableGestures = disableGestures, confirmationDelay = confirmationDelay,
data = data, kind = kind, verifiedState = verifiedState, nonce = nonce,
)
}
}
}

data class CashBill(
override val token: Token,
override val amount: LocalFiat,
override val didReceive: Boolean = false,
override val disableGestures: Boolean = false,
override val confirmationDelay: Duration = Duration.ZERO,
override val data: List<Byte> = emptyList(),
override val kind: Payable.Kind = Payable.Kind.cash,
override val verifiedState: VerifiedState? = null,
override val nonce: List<Byte> = emptyList(),
) : Payable {
override fun stamped(code: List<Byte>, nonce: List<Byte>) = copy(data = code, nonce = nonce)
}

data class GoldBar(
override val token: Token,
override val amount: LocalFiat,
override val didReceive: Boolean = false,
override val disableGestures: Boolean = false,
override val confirmationDelay: Duration = Duration.ZERO,
override val data: List<Byte> = emptyList(),
override val kind: Payable.Kind = Payable.Kind.cash,
override val verifiedState: VerifiedState? = null,
override val nonce: List<Byte> = emptyList(),
) : Payable {
override fun stamped(code: List<Byte>, nonce: List<Byte>) = copy(data = code, nonce = nonce)
}

data class TipCard(
override val data: List<Byte>,
val username: String,
) : Scannable
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class BillStateTest {

@Test
fun `canSwipeToDismiss returns true when bill allows gestures`() {
val bill = Bill.Cash(
val bill = Scannable.CashBill(
token = testToken(),
amount = testLocalFiat(),
disableGestures = false,
Expand All @@ -77,7 +77,7 @@ class BillStateTest {

@Test
fun `canSwipeToDismiss returns false when bill disableGestures is true`() {
val bill = Bill.Cash(
val bill = Scannable.CashBill(
token = testToken(),
amount = testLocalFiat(),
disableGestures = true,
Expand All @@ -100,7 +100,7 @@ class BillStateTest {

@Test
fun `confirmationDelayMillis converts Duration correctly`() {
val bill = Bill.Cash(
val bill = Scannable.CashBill(
token = testToken(),
amount = testLocalFiat(),
confirmationDelay = 3.seconds,
Expand All @@ -119,15 +119,15 @@ class BillStateTest {

// endregion

// region Bill.Cash metadata and canFlip
// region Scannable.CashBill metadata and canFlip

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

val bill = Bill.Cash(
val bill = Scannable.CashBill(
token = token,
amount = amount,
data = data,
Expand All @@ -142,7 +142,7 @@ class BillStateTest {

@Test
fun `Bill Cash canFlip is always false`() {
val bill = Bill.Cash(
val bill = Scannable.CashBill(
token = testToken(),
amount = testLocalFiat(),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.flipcash.app.core.bill

import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.HolderMetrics
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.MintMetadata
import com.getcode.opencode.model.financial.Rate
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.VmMetadata
import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.PublicKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds

class ScannableTest {

private fun tokenWith(mint: Mint): Token = MintMetadata(
address = mint,
decimals = 6,
name = "Test",
symbol = "TST",
createdAt = null,
description = "",
imageUrl = "",
vmMetadata = VmMetadata(
vm = PublicKey.fromBase58("11111111111111111111111111111111"),
authority = PublicKey.fromBase58("11111111111111111111111111111111"),
lockDurationInDays = 21
),
launchpadMetadata = null,
billCustomizations = null,
socialLinks = emptyList(),
holderMetrics = HolderMetrics.None,
)

private fun localFiat(): LocalFiat = LocalFiat(
underlyingTokenAmount = Fiat(5.0),
nativeAmount = Fiat(5.0),
rate = Rate.oneToOne,
mint = Mint.usdf,
)

private val nonUsdfMint = Mint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") // USDC — not USDF

@Test
fun `forToken returns GoldBar for usdf mint`() {
val bill = Scannable.Payable.forToken(token = tokenWith(Mint.usdf), amount = localFiat())
assertIs<Scannable.GoldBar>(bill)
}

@Test
fun `forToken returns CashBill for non-usdf mint`() {
val bill = Scannable.Payable.forToken(token = tokenWith(nonUsdfMint), amount = localFiat())
assertIs<Scannable.CashBill>(bill)
}

@Test
fun `forToken preserves passed-through fields`() {
val expectedData = listOf<Byte>(1, 2, 3)
val expectedNonce = listOf<Byte>(9)
val expectedDelay = 4.seconds
val bill = Scannable.Payable.forToken(
token = tokenWith(nonUsdfMint),
amount = localFiat(),
didReceive = true,
disableGestures = true,
confirmationDelay = expectedDelay,
data = expectedData,
kind = Scannable.Payable.Kind.airdrop,
nonce = expectedNonce,
)
assertTrue(bill.didReceive)
assertTrue(bill.disableGestures)
assertEquals(expectedDelay, bill.confirmationDelay)
assertEquals(expectedData, bill.data)
assertEquals(Scannable.Payable.Kind.airdrop, bill.kind)
assertEquals(expectedNonce, bill.nonce)
}

@Test
fun `stamped replaces code and nonce on CashBill and keeps its type`() {
val bill = Scannable.CashBill(token = tokenWith(nonUsdfMint), amount = localFiat())
val code = listOf<Byte>(1, 2, 3)
val nonce = listOf<Byte>(9, 8)
val stamped = bill.stamped(code = code, nonce = nonce)
assertIs<Scannable.CashBill>(stamped)
assertEquals(code, stamped.data)
assertEquals(nonce, stamped.nonce)
assertEquals(bill.token, stamped.token)
}

@Test
fun `stamped keeps GoldBar type`() {
val bar = Scannable.GoldBar(token = tokenWith(Mint.usdf), amount = localFiat())
val stamped = bar.stamped(code = listOf(7), nonce = emptyList())
assertIs<Scannable.GoldBar>(stamped)
assertEquals(listOf<Byte>(7), stamped.data)
assertEquals(bar.token, stamped.token)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.bill.customization.components.BillPlayground
import com.flipcash.app.bills.AnimatedBill
import com.flipcash.app.core.bill.Bill
import com.flipcash.app.core.bill.Scannable
import com.flipcash.features.bill.playground.R
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.AppBarDefaults
Expand Down Expand Up @@ -82,7 +82,6 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
derivedStateOf {
if (!playgroundState.context.renderAsOverlay) return@derivedStateOf null
val bill = playgroundState.bill ?: return@derivedStateOf null
if (bill !is Bill.Cash) return@derivedStateOf null
bill.copy(
token = bill.token.copy(
billCustomizations = customizationsOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.core.bill.Bill
import com.flipcash.app.core.bill.Scannable
import com.flipcash.app.core.tokens.SwapPurpose
import com.flipcash.app.core.ui.CurrencyHolder
import com.flipcash.app.tokens.TokenCoordinator
Expand Down Expand Up @@ -105,7 +105,7 @@ internal class CashScreenViewModel @Inject constructor(
data class OnCurrencyChanged(val model: com.getcode.opencode.model.financial.Currency) : Event
data class OnLimitsChanged(val limits: Limits?) : Event
data object OnGive : Event
data class PresentBill(val bill: Bill.Cash) : Event
data class PresentBill(val bill: Scannable.Payable) : Event

data class AddCashToWallet(val amount: Fiat) : Event
data class UpdateLoadingState(val loading: Boolean = false, val success: Boolean = false) :
Expand Down Expand Up @@ -260,7 +260,7 @@ internal class CashScreenViewModel @Inject constructor(
return@onEach
}

val bill = Bill.Cash(
val bill = Scannable.Payable.forToken(
token = stateFlow.value.token!!.token,
amount = result.localFiat,
verifiedState = result.verifiedState,
Expand Down
Loading
Loading