From f3ac68532615ffc991fc47f7a1d148f8f9015572 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 20 Jul 2026 14:09:58 -0400 Subject: [PATCH] fix(currencycreator): rebuild bill post purchase with correct verified state for grabbing immediately Signed-off-by: Brandon McAnsh --- .../CurrencyCreatorFlowScreen.kt | 20 +++----- .../internal/CurrencyCreatorViewModel.kt | 49 +++++++++++++++++-- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt index dce8ec39cf..4f8154abd1 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt @@ -27,7 +27,6 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import com.flipcash.app.bill.customization.LocalBillPlaygroundController import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.bill.Bill import com.flipcash.app.core.tokens.CurrencyCreatorResult import com.flipcash.app.core.tokens.CurrencyCreatorStep import com.flipcash.app.currencycreator.internal.screens.BillCustomizationContent @@ -62,7 +61,6 @@ import com.getcode.navigation.flow.deliverFlowResult import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.NavResultStateRegistry import com.getcode.opencode.model.financial.Fiat -import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.toFiat import com.getcode.theme.CodeTheme import com.getcode.theme.rememberDynamicAccent @@ -181,17 +179,13 @@ fun CurrencyCreatorFlowScreen( ) if (result is CurrencyCreatorResult.Success) { - if (state.purchaseAmount > Fiat.Zero) { - val token = state.launchedToken - if (token != null) { - val bill = Bill.Cash( - token = token, - amount = LocalFiat.fromUsd(usdf = state.purchaseAmount), - didReceive = true, - ) - outerNavigator.hide() - session?.showBill(bill) - } + // The bill is priced through the new token's bonding curve in the + // ViewModel (see buildLaunchBill); presenting it lets someone grab the + // newly created currency. It's null if pricing failed — just close then. + val bill = state.launchBill + if (state.purchaseAmount > Fiat.Zero && bill != null) { + outerNavigator.hide() + session?.showBill(bill) } else { outerNavigator.pop() } 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 4178ebd9f3..f6abed44ec 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 @@ -88,7 +88,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( moderationController: ModerationController, currencyController: CurrencyController, private val verifiedFiatCalculator: VerifiedFiatCalculator, - tokenCoordinator: TokenCoordinator, + private val tokenCoordinator: TokenCoordinator, balancePoller: BalancePoller, private val resources: ResourceHelper, val contentReader: ContentReader, @@ -113,6 +113,9 @@ internal class CurrencyCreatorViewModel @Inject constructor( val bill: Bill? = null, val createdMint: Mint? = null, val launchedToken: Token? = null, + // The celebratory give-bill for the freshly launched currency, priced through the + // token's bonding curve (not USDF 1:1) and carrying the verified state a grab needs. + val launchBill: Bill.Cash? = null, val purchaseAmount: Fiat = 5.toFiat(), val feeAmount: Fiat? = null, val processingState: LoadingSuccessState = LoadingSuccessState(), @@ -189,7 +192,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( data class ConfirmPurchase(val fundedWith: Mint): Event data class PurchaseSubmitted(val swapId: SwapId, val mint: Mint) : Event - data class PurchaseCompleted(val token: Token): Event + data class PurchaseCompleted(val token: Token, val bill: Bill.Cash?): Event data object OnIntroContinue : Event data object AdvanceFromInfo : Event @@ -608,9 +611,14 @@ internal class CurrencyCreatorViewModel @Inject constructor( ).map { event.mint } }.flatMapResult { mint -> tokenCoordinator.getTokenMetadata(mint) + }.flatMapResult { result -> + // Build the celebratory give-bill for the new currency. A failure to price it + // (e.g. stale rates) must not fail the launch — the currency was created — so + // carry a null bill and let the flow simply close instead. + Result.success(result.token to buildLaunchBill(result.token)) }.onResult( - onSuccess = { result -> - dispatchEvent(Event.PurchaseCompleted(result.token)) + onSuccess = { (token, bill) -> + dispatchEvent(Event.PurchaseCompleted(token, bill)) dispatchEvent(Event.UpdateProcessingState(success = true)) }, onError = { @@ -619,6 +627,37 @@ internal class CurrencyCreatorViewModel @Inject constructor( ).launchIn(viewModelScope) } + /** + * Builds the give-bill presented after a currency launch. The bill is for the newly + * minted [token], valued at the USD [State.purchaseAmount] but priced through the token's + * bonding curve via [VerifiedFiatCalculator.compute] — the same path the normal give flow + * uses. This yields a [LocalFiat] carrying the token's real mint and exchange rate plus the + * [VerifiedState] the grab intent submits, so the server's "expected sell value" check passes. + * + * Using `LocalFiat.fromUsd()` here instead would hard-code the USDF mint and a 1:1 rate, + * causing the grab to fail with `native amount does not match expected sell value`. + * + * Returns null if the verified fiat can't be computed; the launch still succeeds. + */ + private suspend fun buildLaunchBill(token: Token): Bill.Cash? { + val balance = tokenCoordinator.tokenBalances.firstOrNull() + ?.firstOrNull { it.token.address == token.address } + ?.balance + val verifiedFiat = verifiedFiatCalculator.compute( + amount = stateFlow.value.purchaseAmount, + token = token, + balance = balance, + rate = exchange.preferredRate, + ).getOrNull() ?: return null + + return Bill.Cash( + token = token, + amount = verifiedFiat.localFiat, + verifiedState = verifiedFiat.verifiedState, + didReceive = true, + ) + } + /** * Connect the ViewModel to the top bar controller. The ViewModel pushes * [State.progress] to the controller whenever the current step changes. @@ -706,7 +745,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( is Event.OpenScreen -> { state -> state } is Event.PurchaseSubmitted -> { state -> state } is Event.PurchaseCompleted -> { state -> - state.copy(launchedToken = event.token) + state.copy(launchedToken = event.token, launchBill = event.bill) } } }