diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt
index cd29dd2a5..01e639775 100644
--- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt
+++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt
@@ -28,7 +28,5 @@ sealed interface SwapStep : FlowStep, Parcelable {
data object PhantomConfirmTransaction: SwapStep
@Parcelize
@Serializable
- data class Processing(
- val swapId: SwapId,
- ) : SwapStep, NonDismissableRoute, NonDraggableRoute
+ data object Processing : SwapStep, NonDismissableRoute, NonDraggableRoute
}
diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index e93d3261b..ea1791795 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -234,6 +234,8 @@
Something Went Wrong
Something Went Wrong
Transaction Failed
+ Transaction Expired
+ The transaction took too long to process. Please try again
Something Went Wrong
Something Went Wrong
Request Rejected in %1$s
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt
index 6e433dcef..44776c399 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt
@@ -132,8 +132,8 @@ internal fun PhantomTransactionConfirmationScreen() {
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance()
- .onEach { event ->
- flowNavigator.navigateTo(SwapStep.Processing(event.swapId))
+ .onEach {
+ flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt
index 79de8b9d7..9967560c7 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt
@@ -89,9 +89,8 @@ internal fun SwapEntryScreen(
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance()
- .map { it.swapId }
- .onEach { swapId ->
- flowNavigator.navigateTo(SwapStep.Processing(swapId))
+ .onEach {
+ flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}
@@ -130,9 +129,7 @@ internal fun SwapEntryScreen(
coinbaseOnRampController.pendingNavigation.collect { route ->
if (route is AppRoute.Token.TxProcessing) {
viewModel.dispatchEvent(SwapViewModel.Event.OnSwapIdChanged(route.swapId))
- flowNavigator.navigateTo(
- SwapStep.Processing(route.swapId)
- )
+ flowNavigator.navigateTo(SwapStep.Processing)
}
}
}
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt
index 471a81ca6..73c18e075 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt
@@ -61,7 +61,7 @@ private fun swapEntryProvider(): (NavKey) -> NavEntry = entryProvider {
annotatedEntry {
PhantomTransactionConfirmationScreen()
}
- annotatedEntry { step ->
- SwapProcessingScreen(step.swapId)
+ annotatedEntry {
+ SwapProcessingScreen()
}
}
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt
index d8250c310..0c7380803 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt
@@ -52,9 +52,8 @@ internal fun SellReceiptScreen() {
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance()
- .map { it.swapId }
- .onEach { swapId ->
- flowNavigator.navigateTo(SwapStep.Processing(swapId))
+ .onEach {
+ flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}
}
diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt
index 1bc8fb219..2330df8c7 100644
--- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt
+++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt
@@ -24,18 +24,12 @@ import kotlinx.coroutines.flow.onEach
* Flow-aware swap processing content, used inside `SwapFlowScreen`.
*/
@Composable
-internal fun SwapProcessingScreen(
- swapId: SwapId,
-) {
+internal fun SwapProcessingScreen() {
val flowNavigator = rememberFlowNavigator()
val viewModel = flowSharedViewModel()
TokenTxProcessingScreen(viewModel = viewModel)
- LaunchedEffect(viewModel) {
- viewModel.dispatchEvent(Event.UpdateProcessingState(loading = true))
- }
-
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance()
@@ -52,6 +46,14 @@ internal fun SwapProcessingScreen(
}.launchIn(this)
}
+ LaunchedEffect(viewModel) {
+ viewModel.eventFlow
+ .filterIsInstance()
+ .onEach {
+ flowNavigator.exitCanceled()
+ }.launchIn(this)
+ }
+
BackHandler { /* intercept */ }
}
diff --git a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/DeeplinkOnRampErrors.kt b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/DeeplinkOnRampErrors.kt
index 08dd68352..0d7afbd6e 100644
--- a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/DeeplinkOnRampErrors.kt
+++ b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/DeeplinkOnRampErrors.kt
@@ -25,6 +25,11 @@ sealed class DeeplinkOnRampError(
override val cause: Throwable? = null
) : DeeplinkOnRampError(code = code, message = message, cause = cause)
+ class TransactionExpired(
+ override val message: String?,
+ override val cause: Throwable? = null
+ ) : DeeplinkOnRampError(message = message, cause = cause)
+
class FailedToSubmitBuyToServer(
override val code: Long = -100,
override val message: String?,
@@ -70,6 +75,7 @@ val DeeplinkOnRampError.isAlert: Boolean
DeeplinkError.Disconnected,
DeeplinkError.TransactionRejected,
) || this is DeeplinkOnRampError.FailedToSendTransaction
+ || this is DeeplinkOnRampError.TransactionExpired
|| this is DeeplinkOnRampError.InsufficientSol
|| this is DeeplinkOnRampError.InsufficientUsdc
|| (this is DeeplinkOnRampError.FailedToSimulateTransaction && cause?.isNetworkError() == true)
@@ -83,6 +89,7 @@ fun DeeplinkOnRampError.messaging(getString: (Int) -> String, provider: String):
is DeeplinkOnRampError.FailedToCreateTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToCreateTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToCreateTransaction)
is DeeplinkOnRampError.FailedToSimulateTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToSimulateTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToSimulateTransaction)
is DeeplinkOnRampError.FailedToSendTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToSendTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToSendTransaction).format(provider)
+ is DeeplinkOnRampError.TransactionExpired -> getString(R.string.error_title_deeplinkOnRampTransactionExpired) to getString(R.string.error_description_deeplinkOnRampTransactionExpired)
is DeeplinkOnRampError.FailedToSubmitBuyToServer -> getString(R.string.error_title_deeplinkOnRampExternalFundBuy) to getString(R.string.error_description_deeplinkOnRampExternalFundBuy).format(provider)
is DeeplinkOnRampError.InsufficientSol -> getString(R.string.error_title_deeplinkOnRampInsufficientSol) to getString(R.string.error_description_deeplinkOnRampInsufficientSol).format(provider)
is DeeplinkOnRampError.InsufficientUsdc -> getString(R.string.error_title_deeplinkOnRampInsufficientUsdc) to getString(R.string.error_description_deeplinkOnRampInsufficientUsdc).format(provider)
diff --git a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/PhantomWalletController.kt b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/PhantomWalletController.kt
index eb48b7284..c3d7ec8ce 100644
--- a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/PhantomWalletController.kt
+++ b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/PhantomWalletController.kt
@@ -277,14 +277,30 @@ class PhantomWalletController @Inject constructor(
}
}
)
- ErrorUtils.handleError(error)
- return@withContext Result.failure(
- DeeplinkOnRampError.FailedToSendTransaction(
- code = code ?: -99L,
- message = error.message,
- cause = error,
- )
- )
+
+ when {
+ // Detect expired blockhash — occurs when the Phantom wallet
+ // round-trip exceeds ~60 seconds (Solana's blockhash lifetime).
+ error is RpcException && error.isBlockhashNotFound -> {
+ ErrorUtils.handleError(error)
+ return@withContext Result.failure(
+ DeeplinkOnRampError.TransactionExpired(
+ message = error.message,
+ cause = error,
+ )
+ )
+ }
+ else -> {
+ ErrorUtils.handleError(error)
+ return@withContext Result.failure(
+ DeeplinkOnRampError.FailedToSendTransaction(
+ code = code ?: -99L,
+ message = error.message,
+ cause = error,
+ )
+ )
+ }
+ }
}
Result.success(Unit)
diff --git a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/DeeplinkErrorTest.kt b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/DeeplinkErrorTest.kt
index db7fb5d32..578a8f0dd 100644
--- a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/DeeplinkErrorTest.kt
+++ b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/DeeplinkErrorTest.kt
@@ -75,6 +75,7 @@ class DeeplinkErrorTest {
fun `DeeplinkOnRampError subtypes are Throwable`() {
val errors: List = listOf(
DeeplinkOnRampError.FailedToSendTransaction(message = "fail"),
+ DeeplinkOnRampError.TransactionExpired(message = "expired"),
DeeplinkOnRampError.WalletProvidedError(
error = DeeplinkError.Disconnected,
message = "disconnected"
@@ -87,6 +88,24 @@ class DeeplinkErrorTest {
}
}
+ @Test
+ fun `TransactionExpired isAlert returns true`() {
+ val error = DeeplinkOnRampError.TransactionExpired(message = "Blockhash not found")
+ assertTrue(error.isAlert, "TransactionExpired should be an alert")
+ }
+
+ @Test
+ fun `TransactionExpired preserves cause chain`() {
+ val cause = RuntimeException("Blockhash not found")
+ val error = DeeplinkOnRampError.TransactionExpired(
+ message = cause.message,
+ cause = cause,
+ )
+
+ assertEquals("Blockhash not found", error.message)
+ assertEquals(cause, error.cause)
+ }
+
@Test
fun `WalletProvidedError code matches error code`() {
val error = DeeplinkOnRampError.WalletProvidedError(
diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
index 68a7a1bf1..2a8042913 100644
--- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
+++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt
@@ -982,14 +982,15 @@ class SwapViewModel @Inject constructor(
amount = amount,
fee = LocalFiat.Zero,
token = token,
- onBeforeSign = { swapId ->
+ onBeforeSign = {
analytics.amountSelectedForWalletTransfer(
OnRampProvider.Phantom,
amount.localFiat.underlyingTokenAmount
)
- dispatchEvent(Event.PhantomNavigateToProcessing(swapId))
},
).onSuccess { swapId ->
+ dispatchEvent(Event.UpdateProcessingState(loading = true))
+ dispatchEvent(Event.PhantomNavigateToProcessing(swapId))
dispatchEvent(Event.OnSwapIdChanged(swapId))
}.onFailure { error ->
handlePhantomError(error)
diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt
index 5d58322bb..448202e98 100644
--- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt
+++ b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt
@@ -200,4 +200,8 @@ suspend fun Rpc20Driver.simulateTransaction(
class RpcException(
val code: Int,
override val message: String,
-) : Throwable(message)
\ No newline at end of file
+) : Throwable(message) {
+ val isBlockhashNotFound: Boolean
+ get() = message.contains("Blockhash not found", ignoreCase = true) ||
+ message.contains("BlockhashNotFound", ignoreCase = true)
+}
\ No newline at end of file