Skip to content

Commit f406304

Browse files
committed
feat(buy): consolidate amount entry into single screen
Now the Buy button navigates directly to the Swap amount screen. SwapViewModel checks reserves on confirm: sufficient buys from reserves, insufficient presents purchase method sheet on the same screen. Shortfall flow skips TokenInfo and pre-fills the amount with a minimum enforced. Single available purchase method auto-selects without showing the sheet. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 008f752 commit f406304

16 files changed

Lines changed: 268 additions & 61 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ sealed interface AppRoute : NavKey, Parcelable {
137137
val shortfall: Fiat? = null,
138138
) : Token, FlowRouteWithResult<SwapResult> {
139139
override val initialStack: List<NavKey>
140-
get() = listOf(SwapStep.Entry(purpose))
140+
get() = listOf(SwapStep.Entry(purpose, initialAmount = shortfall))
141141
}
142142

143143
@Serializable

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import com.getcode.navigation.NonDismissableRoute
55
import com.getcode.navigation.NonDraggableRoute
66
import com.getcode.navigation.flow.FlowStep
77
import com.getcode.opencode.internal.solana.model.SwapId
8+
import com.getcode.opencode.model.financial.Fiat
89
import kotlinx.parcelize.Parcelize
910
import kotlinx.serialization.Serializable
1011

1112
@Serializable
1213
sealed interface SwapStep : FlowStep, Parcelable {
1314
@Parcelize
1415
@Serializable
15-
data class Entry(val purpose: SwapPurpose) : SwapStep
16+
data class Entry(val purpose: SwapPurpose, val initialAmount: Fiat? = null) : SwapStep
1617

1718
@Parcelize
1819
@Serializable

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@
439439
<string name="title_amountToBuy">Amount to Buy</string>
440440
<string name="title_amountToSell">Amount to Sell</string>
441441
<string name="subtitle_buySellCashHint">Enter up to %1$s</string>
442+
<string name="subtitle_buyHintBelowMinimum">You must buy at least %1$s</string>
442443
<string name="subtitle_buyHintLimitExceeded">You can only buy up to %1$s</string>
443444
<string name="subtitle_sellHintLimitExceeded">You can only sell up to %1$s</string>
444445
<string name="action_buy">Buy</string>

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.flipcash.app.cash.internal
33
import androidx.lifecycle.viewModelScope
44
import com.flipcash.app.core.AppRoute
55
import com.flipcash.app.core.bill.Bill
6+
import com.flipcash.app.core.tokens.SwapPurpose
67
import com.flipcash.app.core.ui.CurrencyHolder
78
import com.flipcash.app.tokens.TokenCoordinator
89
import com.flipcash.features.cash.R
@@ -338,12 +339,12 @@ internal class CashScreenViewModel @Inject constructor(
338339
.filterIsInstance<Event.AddCashToWallet>()
339340
.map { it.amount }
340341
.onEach { shortfall ->
341-
// route to buy the token
342-
println("shortfall=$shortfall")
342+
// route directly to the swap amount screen, skipping token info
343+
val mint = stateFlow.value.selectedTokenAddress!!
343344
dispatchEvent(
344345
Event.OpenScreen(
345-
AppRoute.Token.Info(
346-
mint = stateFlow.value.selectedTokenAddress!!,
346+
AppRoute.Token.Swap(
347+
purpose = SwapPurpose.Buy(mint),
347348
shortfall = shortfall,
348349
),
349350
)

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,6 @@ internal class CurrencyCreatorViewModel @Inject constructor(
432432
eventFlow
433433
.filterIsInstance<Event.LaunchToken>()
434434
.map { event ->
435-
println("customizations=${stateFlow.value.customizations}")
436435
val request = TokenCreateRequest(
437436
name = ModerationAttestation.Text(
438437
text = stateFlow.value.nameFieldState.text.trim().toString(),

apps/flipcash/features/tokens/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies {
1010
implementation(libs.bundles.haze)
1111

1212
implementation(project(":apps:flipcash:shared:analytics"))
13+
implementation(project(":apps:flipcash:shared:onramp:coinbase"))
1314
implementation(project(":apps:flipcash:shared:onramp:deeplinks"))
1415
implementation(project(":apps:flipcash:shared:shareable"))
1516
implementation(project(":apps:flipcash:shared:tokens"))

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryContent.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ import com.flipcash.app.core.AppRoute
1313
import com.flipcash.app.core.tokens.SwapPurpose
1414
import com.flipcash.app.core.tokens.SwapResult
1515
import com.flipcash.app.core.tokens.SwapStep
16+
import com.flipcash.app.onramp.LocalCoinbaseOnRampController
1617
import com.flipcash.app.onramp.LocalExternalWalletOnRampController
1718
import com.flipcash.app.tokens.internal.SwapEntryScreenContent
1819
import com.flipcash.app.tokens.ui.SwapViewModel
1920
import com.flipcash.features.tokens.R
21+
import com.getcode.navigation.core.LocalCodeNavigator
2022
import com.getcode.navigation.flow.flowSharedViewModel
2123
import com.getcode.navigation.flow.rememberFlowNavigator
24+
import com.getcode.opencode.model.financial.Fiat
2225
import com.getcode.ui.components.AppBarWithTitle
2326
import kotlinx.coroutines.flow.filterIsInstance
2427
import kotlinx.coroutines.flow.launchIn
@@ -28,11 +31,14 @@ import kotlinx.coroutines.flow.onEach
2831
@Composable
2932
internal fun SwapEntryContent(
3033
purpose: SwapPurpose,
34+
initialAmount: Fiat? = null,
3135
) {
3236
val flowNavigator = rememberFlowNavigator<SwapStep, SwapResult>()
3337
val viewModel = flowSharedViewModel<SwapViewModel>()
3438
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
39+
val navigator = LocalCodeNavigator.current
3540
val externalWalletOnRampController = LocalExternalWalletOnRampController.current
41+
val coinbaseOnRampController = LocalCoinbaseOnRampController.current
3642

3743
Column(
3844
modifier = Modifier.fillMaxSize(),
@@ -60,6 +66,9 @@ internal fun SwapEntryContent(
6066

6167
LaunchedEffect(viewModel) {
6268
viewModel.dispatchEvent(SwapViewModel.Event.OnPurposeChanged(purpose))
69+
if (initialAmount != null) {
70+
viewModel.dispatchEvent(SwapViewModel.Event.OnInitialAmountProvided(initialAmount))
71+
}
6372
}
6473

6574
LaunchedEffect(viewModel) {
@@ -106,4 +115,29 @@ internal fun SwapEntryContent(
106115
}
107116
}
108117
}
118+
119+
LaunchedEffect(viewModel) {
120+
viewModel.eventFlow
121+
.filterIsInstance<SwapViewModel.Event.OnVerificationNeeded>()
122+
.onEach { (phone, email) ->
123+
val mint = (viewModel.stateFlow.value.purpose as? SwapPurpose.Buy)?.mint ?: return@onEach
124+
navigator.push(
125+
AppRoute.Verification(
126+
origin = AppRoute.Token.OnRamp(mint),
127+
includePhone = phone,
128+
includeEmail = email,
129+
)
130+
)
131+
}.launchIn(this)
132+
}
133+
134+
LaunchedEffect(Unit) {
135+
coinbaseOnRampController.pendingNavigation.collect { route ->
136+
if (route is AppRoute.Token.TxProcessing) {
137+
flowNavigator.navigateTo(
138+
SwapStep.Processing(route.swapId, route.awaitExternalWallet)
139+
)
140+
}
141+
}
142+
}
109143
}

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fun SwapFlowScreen(
5252

5353
private fun swapEntryProvider(): (NavKey) -> NavEntry<NavKey> = entryProvider {
5454
flowAnnotatedEntry<SwapStep.Entry> { step ->
55-
SwapEntryContent(step.purpose)
55+
SwapEntryContent(step.purpose, step.initialAmount)
5656
}
5757
annotatedEntry<SwapStep.SellReceipt> { SellReceiptContent() }
5858
annotatedEntry<SwapStep.Processing> { step ->

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/SwapEntryScreenContent.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ internal fun SwapEntryScreenContent(
5555
currencyFlag = entryState.currencyModel.selected?.resId,
5656
prefix = entryState.currencyModel.selected?.symbol.orEmpty(),
5757
placeholder = "0",
58-
hint = if (state.isError) {
58+
hint = if (state.isBelowMinimum) {
59+
stringResource(
60+
R.string.subtitle_buyHintBelowMinimum,
61+
state.minimumBuyAmount?.formatted().orEmpty()
62+
)
63+
} else if (state.isError) {
5964
when (state.purpose) {
6065
is SwapPurpose.BalanceIncrease -> stringResource(
6166
R.string.subtitle_buyHintLimitExceeded,

apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodController.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ interface PurchaseMethodController {
77
val state: StateFlow<PurchaseMethodState>
88
val selections: Flow<PurchaseMethodSelection>
99
fun present(metadata: PurchaseMethodMetadata = PurchaseMethodMetadata())
10+
fun select(method: PurchaseMethod, metadata: PurchaseMethodMetadata)
1011
}

0 commit comments

Comments
 (0)