Skip to content

Commit c18ad34

Browse files
committed
fix(onramp/coinbase): poll lookupOrder for txHash + pass amount through TxProcessing
lookupOrder() is called once after payment but txHash may not yet be populated by Coinbase. Poll with 3s intervals (100 attempts / ~5min timeout) using the new pollUntil utility until txHash appears. Also threads the purchase amount from CoinbaseOnRampState.Completed through to TxProcessing and SwapViewModel so the UI can display it. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 91faf34 commit c18ad34

12 files changed

Lines changed: 130 additions & 25 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fun appEntryProvider(
109109
SwapFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
110110
}
111111
annotatedEntry<AppRoute.Token.TxProcessing> { key ->
112-
TokenTxProcessingScreen(key.swapId, key.swapPurpose, key.awaitExternalWallet, key.isFundingShortfall)
112+
TokenTxProcessingScreen(key.swapId, key.swapPurpose, key.amount, key.awaitExternalWallet, key.isFundingShortfall)
113113
}
114114
annotatedEntry<AppRoute.Token.OnRamp> { key -> OnRampCustomAmountScreen(key.mint) }
115115
annotatedEntry<AppRoute.Token.Discovery> { TokenDiscoveryScreen() }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.flipcash.app.core.withdrawal.WithdrawalStep
1616
import com.getcode.navigation.NonDismissableRoute
1717
import com.getcode.navigation.NonDraggableRoute
1818
import com.getcode.navigation.flow.FlowRouteWithResult
19+
import com.getcode.opencode.exchange.VerifiedFiat
1920
import com.getcode.opencode.internal.solana.model.SwapId
2021
import com.getcode.opencode.model.financial.Fiat
2122
import com.getcode.solana.keys.Mint
@@ -139,6 +140,7 @@ sealed interface AppRoute : NavKey, Parcelable {
139140
data class TxProcessing(
140141
val swapId: SwapId,
141142
val swapPurpose: SwapPurpose? = null,
143+
val amount: VerifiedFiat? = null,
142144
val awaitExternalWallet: Boolean = false,
143145
val isFundingShortfall: Boolean = false,
144146
) : Token, NonDismissableRoute, NonDraggableRoute

apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/OnRampCustomAmountScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
44
import androidx.compose.foundation.layout.fillMaxSize
55
import androidx.compose.runtime.Composable
66
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.getValue
78
import androidx.compose.ui.Alignment
89
import androidx.compose.ui.Modifier
910
import androidx.compose.ui.res.stringResource
@@ -14,6 +15,7 @@ import com.flipcash.app.onramp.internal.OnRampViewModel
1415
import com.flipcash.app.onramp.internal.screens.OnRampAmountScreen
1516
import com.flipcash.features.onramp.R
1617
import androidx.hilt.navigation.compose.hiltViewModel
18+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1719
import com.getcode.navigation.core.LocalCodeNavigator
1820
import com.getcode.ui.components.AppBarWithTitle
1921
import kotlinx.coroutines.flow.filterIsInstance
@@ -26,13 +28,14 @@ fun OnRampCustomAmountScreen(mint: Mint) {
2628
val navigator = LocalCodeNavigator.current
2729
val viewModel = hiltViewModel<OnRampViewModel>()
2830

31+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
2932
Column(
3033
modifier = Modifier.fillMaxSize(),
3134
) {
3235
AppBarWithTitle(
3336
title = stringResource(R.string.title_amountToBuy),
3437
isInModal = true,
35-
backButton = true,
38+
backButton = state.orderLookup.isIdle,
3639
onBackIconClicked = { navigator.pop() },
3740
titleAlignment = Alignment.CenterHorizontally,
3841
)

apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ internal class OnRampViewModel @Inject constructor(
9898
val hasVerifiedEmail: Boolean = false,
9999
val selectedProvider: OnRampProvider.ThirdParty? = null,
100100
val amountEntryState: AmountEntryState = AmountEntryState(),
101+
val orderLookup: LoadingSuccessState = LoadingSuccessState(),
101102
) {
102103
val minimumPurchaseAmount = 5.toFiat()
103104
}
@@ -132,6 +133,11 @@ internal class OnRampViewModel @Inject constructor(
132133
val success: Boolean = false
133134
) : Event
134135

136+
data class UpdateOrderLookupState(
137+
val loading: Boolean = false,
138+
val success: Boolean = false
139+
): Event
140+
135141
data class OnAmountAccepted(val amount: VerifiedFiat) : Event
136142

137143
data class CreateAndSendTransactionToWallet(val amount: VerifiedFiat) : Event
@@ -158,10 +164,13 @@ internal class OnRampViewModel @Inject constructor(
158164
numberInputHelper.reset()
159165

160166
onRampController.state
161-
.filter { it !is CoinbaseOnRampState.Paying }
162-
.onEach {
163-
if (stateFlow.value.amountEntryState.confirmingAmount.loading) {
164-
dispatchEvent(Event.UpdateConfirmingAmountState())
167+
.onEach { s ->
168+
when (s) {
169+
is CoinbaseOnRampState.Completed -> dispatchEvent(Event.UpdateOrderLookupState(success = true))
170+
is CoinbaseOnRampState.Failed -> dispatchEvent(Event.UpdateOrderLookupState())
171+
CoinbaseOnRampState.Idle -> dispatchEvent(Event.UpdateOrderLookupState())
172+
is CoinbaseOnRampState.Paying -> dispatchEvent(Event.UpdateOrderLookupState(loading = true))
173+
is CoinbaseOnRampState.Processing -> dispatchEvent(Event.UpdateOrderLookupState(loading = true))
165174
}
166175
}
167176
.launchIn(viewModelScope)
@@ -452,6 +461,16 @@ internal class OnRampViewModel @Inject constructor(
452461
)
453462
}
454463

464+
is Event.UpdateOrderLookupState -> { state ->
465+
val lookupState = state.orderLookup
466+
state.copy(
467+
orderLookup = lookupState.copy(
468+
loading = event.loading,
469+
success = event.success,
470+
)
471+
)
472+
}
473+
455474
is Event.OnVerificationNeeded,
456475
is Event.CreateAndSendTransactionToWallet,
457476
Event.OnAmountConfirmed,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import com.flipcash.app.tokens.ui.SwapViewModel.Event
2121
import com.getcode.navigation.core.LocalCodeNavigator
2222
import com.getcode.navigation.flow.flowSharedViewModel
2323
import com.getcode.navigation.flow.rememberFlowNavigator
24+
import com.getcode.opencode.exchange.VerifiedFiat
2425
import com.getcode.opencode.internal.solana.model.SwapId
26+
import com.getcode.opencode.model.financial.LocalFiat
2527
import com.getcode.view.LoadingSuccessState
2628
import kotlinx.coroutines.flow.filterIsInstance
2729
import kotlinx.coroutines.flow.firstOrNull
@@ -98,6 +100,7 @@ internal fun SwapProcessingContent(
98100
fun TokenTxProcessingScreen(
99101
swapId: SwapId,
100102
swapPurpose: SwapPurpose?,
103+
swapAmount: VerifiedFiat?,
101104
awaitExternalWallet: Boolean = false,
102105
isFundingShortfall: Boolean = false,
103106
) {
@@ -136,6 +139,9 @@ fun TokenTxProcessingScreen(
136139
if (swapPurpose != null) {
137140
viewModel.dispatchEvent(Event.OnPurposeChanged(swapPurpose))
138141
}
142+
if (swapAmount != null) {
143+
viewModel.dispatchEvent(Event.OnAmountAccepted(swapAmount, swapAmount.localFiat.nativeAmount))
144+
}
139145
}
140146
}
141147

apps/flipcash/shared/onramp/coinbase/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ dependencies {
2424
implementation(project(":apps:flipcash:shared:web"))
2525
api(project(":libs:network:coinbase:onramp"))
2626
implementation(project(":libs:network:jwt"))
27+
implementation(project(":libs:network:connectivity:public"))
2728
}

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import com.getcode.opencode.model.financial.Token
2626
import com.getcode.opencode.model.financial.usdf
2727
import com.getcode.opencode.model.transactions.SwapFundingSource
2828
import com.getcode.solana.keys.base58
29-
import com.getcode.utils.base64
29+
import com.getcode.utils.network.pollUntil
3030
import com.getcode.vendor.Base58
3131
import com.flipcash.app.core.AppRoute
3232
import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError
@@ -43,6 +43,7 @@ import kotlinx.serialization.json.Json
4343
import retrofit2.HttpException
4444
import java.security.SecureRandom
4545
import javax.inject.Inject
46+
import kotlin.time.Duration.Companion.seconds
4647

4748
typealias OrderWithPaymentLink = Pair<String, OnRampPurchaseResponse.PaymentLink>
4849

@@ -121,11 +122,15 @@ class CoinbaseOnRampController @Inject constructor(
121122
return Result.failure(IllegalStateException("Not in Processing state"))
122123
}
123124

124-
return lookupOrder(current.orderId)
125-
.mapCatching { order ->
125+
return pollUntil(
126+
call = { lookupOrder(current.orderId).getOrThrow() },
127+
required = { order -> order.txHash != null },
128+
maxAttempts = 100,
129+
interval = 3.seconds,
130+
tag = "CoinbaseOrderPoller",
131+
).mapCatching { order ->
126132
order.txHash ?: throw IllegalStateException("No hash provided from provider")
127-
}
128-
.mapCatching { txHash ->
133+
}.mapCatching { txHash ->
129134
val owner = userManager.accountCluster
130135
?: throw IllegalStateException("No account cluster")
131136

@@ -142,7 +147,7 @@ class CoinbaseOnRampController @Inject constructor(
142147
).getOrThrow()
143148
}
144149
.onSuccess { swapId ->
145-
_state.update { CoinbaseOnRampState.Completed(swapId, current.token) }
150+
_state.update { CoinbaseOnRampState.Completed(swapId, current.token, current.amount) }
146151
}
147152
.onFailure {
148153
reset()

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fun CoinbaseOnRampHandler(
5353
is CoinbaseOnRampState.Completed -> {
5454
LaunchedEffect(current) {
5555
controller.emitPendingNavigation(
56-
AppRoute.Token.TxProcessing(current.swapId, SwapPurpose.Buy(current.token.address))
56+
AppRoute.Token.TxProcessing(current.swapId, SwapPurpose.Buy(current.token.address), current.amount)
5757
)
5858
controller.reset()
5959
}

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ sealed interface CoinbaseOnRampState {
1818
data object Idle : CoinbaseOnRampState
1919
data class Paying(val order: OnrampOrder, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState
2020
data class Processing(val orderId: String, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState
21-
data class Completed(val swapId: SwapId, val token: Token) : CoinbaseOnRampState
21+
data class Completed(val swapId: SwapId, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState
2222
data class Failed(val error: CoinbaseOnRampWebError) : CoinbaseOnRampState
2323
}

libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ object BottomBarManager {
128128
private val _messages: MutableStateFlow<List<BottomBarMessage>> = MutableStateFlow(emptyList())
129129
val messages: StateFlow<List<BottomBarMessage>> get() = _messages.asStateFlow()
130130

131-
private fun showMessage(bottomBarMessage: BottomBarMessage) {
131+
@PublishedApi
132+
internal fun showMessage(bottomBarMessage: BottomBarMessage) {
132133
_messages.update { currentMessages ->
133134
currentMessages + bottomBarMessage
134135
}
@@ -281,21 +282,17 @@ object BottomBarManager {
281282
* Additional [BottomBarAction]'s can be included via [actions] and dismiss callbacks
282283
* are available via [onDismiss].
283284
*/
284-
fun showError(
285+
inline fun showError(
285286
title: String,
286287
message: String,
287288
additionalInfo: Map<String, Any?> = emptyMap(),
288289
actions: List<BottomBarAction> = listOf(BottomBarAction.Ok),
289290
showCancel: Boolean = false,
290-
onDismiss: (fromAction: Boolean) -> Unit = { },
291+
noinline onDismiss: (fromAction: Boolean) -> Unit = { },
291292
) {
292293
val callSite = Throwable().stackTrace
293-
.firstOrNull { it.className != BottomBarManager::class.java.name }
294-
?.let {
295-
val file = it.fileName
296-
?: it.className.substringAfterLast('.').substringBefore('$')
297-
"$file:${it.lineNumber}"
298-
}
294+
.firstOrNull { it.fileName != null }
295+
?.let { "${it.fileName}:${it.lineNumber}" }
299296

300297
showMessage(
301298
BottomBarMessage(

0 commit comments

Comments
 (0)