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 @@ -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
}
2 changes: 2 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@
<string name="error_title_deeplinkOnRampFailedToSimulateTransaction">Something Went Wrong</string>
<string name="error_title_deeplinkOnRampFailedToCreateDeeplink">Something Went Wrong</string>
<string name="error_title_deeplinkOnRampFailedToSendTransaction">Transaction Failed</string>
<string name="error_title_deeplinkOnRampTransactionExpired">Transaction Expired</string>
<string name="error_description_deeplinkOnRampTransactionExpired">The transaction took too long to process. Please try again</string>
<string name="error_title_deeplinkOnRampDisconnected">Something Went Wrong</string>
<string name="error_title_deeplinkOnRampUnauthorized">Something Went Wrong</string>
<string name="error_title_deeplinkOnRampUserRejected">Request Rejected in %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ internal fun PhantomTransactionConfirmationScreen() {
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<SwapViewModel.Event.PhantomNavigateToProcessing>()
.onEach { event ->
flowNavigator.navigateTo(SwapStep.Processing(event.swapId))
.onEach {
flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ internal fun SwapEntryScreen(
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<SwapViewModel.Event.OnPurchaseSubmitted>()
.map { it.swapId }
.onEach { swapId ->
flowNavigator.navigateTo(SwapStep.Processing(swapId))
.onEach {
flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}

Expand Down Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private fun swapEntryProvider(): (NavKey) -> NavEntry<NavKey> = entryProvider {
annotatedEntry<SwapStep.PhantomConfirmTransaction> {
PhantomTransactionConfirmationScreen()
}
annotatedEntry<SwapStep.Processing> { step ->
SwapProcessingScreen(step.swapId)
annotatedEntry<SwapStep.Processing> {
SwapProcessingScreen()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ internal fun SellReceiptScreen() {
LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<SwapViewModel.Event.OnSellSubmitted>()
.map { it.swapId }
.onEach { swapId ->
flowNavigator.navigateTo(SwapStep.Processing(swapId))
.onEach {
flowNavigator.navigateTo(SwapStep.Processing)
}.launchIn(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwapStep, SwapResult>()
val viewModel = flowSharedViewModel<SwapViewModel>()

TokenTxProcessingScreen(viewModel = viewModel)

LaunchedEffect(viewModel) {
viewModel.dispatchEvent(Event.UpdateProcessingState(loading = true))
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<Event.OnTransactionSuccessful>()
Expand All @@ -52,6 +46,14 @@ internal fun SwapProcessingScreen(
}.launchIn(this)
}

LaunchedEffect(viewModel) {
viewModel.eventFlow
.filterIsInstance<Event.PhantomCeremonyFailed>()
.onEach {
flowNavigator.exitCanceled()
}.launchIn(this)
}

BackHandler { /* intercept */ }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class DeeplinkErrorTest {
fun `DeeplinkOnRampError subtypes are Throwable`() {
val errors: List<Throwable> = listOf(
DeeplinkOnRampError.FailedToSendTransaction(message = "fail"),
DeeplinkOnRampError.TransactionExpired(message = "expired"),
DeeplinkOnRampError.WalletProvidedError(
error = DeeplinkError.Disconnected,
message = "disconnected"
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,8 @@ suspend fun Rpc20Driver.simulateTransaction(
class RpcException(
val code: Int,
override val message: String,
) : Throwable(message)
) : Throwable(message) {
val isBlockhashNotFound: Boolean
get() = message.contains("Blockhash not found", ignoreCase = true) ||
message.contains("BlockhashNotFound", ignoreCase = true)
}
Loading