Skip to content

Commit e384e5a

Browse files
committed
chore(tokens): pre-navigate to avoid screen flash when returning from Phantom
Push SwapTransact before opening Phantom for connect and TxProcessingScreen before opening for signing, so the user lands directly on the correct screen when returning to the app. Uses a local loading state override to show the spinner immediately without affecting the indicator's timer. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent be8e784 commit e384e5a

5 files changed

Lines changed: 110 additions & 20 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
@@ -94,7 +94,7 @@ internal fun AppScreenContent(content: @Composable () -> Unit) {
9494
}
9595

9696
register<AppRoute.Token.TxProcessing> {
97-
TokenTxProcessingScreen(it.swapId)
97+
TokenTxProcessingScreen(it.swapId, it.awaitExternalWallet)
9898
}
9999

100100
register<AppRoute.Token.SellReceipt> {

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
@@ -68,7 +68,7 @@ sealed interface AppRoute : ScreenProvider, Parcelable {
6868
data class Info(val mint: Mint, val forNeededFunds: Boolean = false, val fromDeeplink: Boolean = false): Token
6969
data class Transactions(val mint: Mint): Token
7070
data class SwapTransact(val purpose: TokenSwapPurpose, val forNeededFunds: Boolean = false): Token
71-
data class TxProcessing(val swapId: SwapId): Token
71+
data class TxProcessing(val swapId: SwapId, val awaitExternalWallet: Boolean = false): Token
7272
data object SellReceipt: Token
7373
}
7474

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ import android.os.Parcelable
44
import androidx.activity.compose.BackHandler
55
import androidx.compose.runtime.Composable
66
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.setValue
11+
import androidx.compose.runtime.snapshotFlow
712
import cafe.adriel.voyager.core.screen.ScreenKey
813
import cafe.adriel.voyager.core.screen.uniqueScreenKey
14+
import com.flipcash.app.onramp.LocalExternalWalletState
15+
import com.flipcash.app.onramp.internal.ExternalWalletState
916
import com.flipcash.app.tokens.internal.TokenTxProcessingScreen
1017
import com.flipcash.app.tokens.ui.BuySellSwapTokenViewModel
1118
import com.flipcash.app.tokens.ui.BuySellSwapTokenViewModel.Event
@@ -14,14 +21,19 @@ import com.getcode.navigation.extensions.getStackScopedViewModel
1421
import com.getcode.navigation.screens.ModalScreen
1522
import com.getcode.opencode.internal.solana.model.SwapId
1623
import com.getcode.ui.utils.DisableSheetGestures
24+
import com.getcode.view.LoadingSuccessState
1725
import kotlinx.coroutines.flow.filterIsInstance
26+
import kotlinx.coroutines.flow.first
1827
import kotlinx.coroutines.flow.launchIn
1928
import kotlinx.coroutines.flow.onEach
2029
import kotlinx.parcelize.IgnoredOnParcel
2130
import kotlinx.parcelize.Parcelize
2231

2332
@Parcelize
24-
class TokenTxProcessingScreen(val swapId: SwapId) : ModalScreen, Parcelable {
33+
class TokenTxProcessingScreen(
34+
val swapId: SwapId,
35+
val awaitExternalWallet: Boolean = false,
36+
) : ModalScreen, Parcelable {
2537

2638
@IgnoredOnParcel
2739
override val key: ScreenKey = uniqueScreenKey
@@ -33,10 +45,40 @@ class TokenTxProcessingScreen(val swapId: SwapId) : ModalScreen, Parcelable {
3345
override fun ModalContent() {
3446
val navigator = LocalCodeNavigator.current
3547
val viewModel = getStackScopedViewModel<BuySellSwapTokenViewModel>(BuySellFlow.key)
36-
TokenTxProcessingScreen(viewModel)
3748

38-
LaunchedEffect(viewModel, swapId) {
39-
viewModel.dispatchEvent(Event.OnSwapIdChanged(swapId))
49+
// When awaiting external wallet, show a local loading indicator that doesn't
50+
// affect the ViewModel's processingProgress timer. Once OnSwapIdChanged is
51+
// dispatched the ViewModel takes over with its own loading state and fresh timer.
52+
var awaitingWallet by remember { mutableStateOf(awaitExternalWallet) }
53+
54+
TokenTxProcessingScreen(
55+
viewModel = viewModel,
56+
processingProgressOverride = if (awaitingWallet) LoadingSuccessState(loading = true) else null,
57+
)
58+
59+
if (awaitExternalWallet) {
60+
val externalWalletState = LocalExternalWalletState.current
61+
LaunchedEffect(viewModel, swapId) {
62+
// Wait for the transaction to be submitted before starting swap polling
63+
snapshotFlow { externalWalletState.deeplinkState }
64+
.first { it == ExternalWalletState.TRANSACTED }
65+
66+
externalWalletState.reset()
67+
viewModel.dispatchEvent(Event.OnSwapIdChanged(swapId))
68+
69+
// Wait for the ViewModel's own loading state before dropping override.
70+
// Both are LoadingSuccessState(loading=true) — data class equality means
71+
// the indicator's remember(processingState) won't reset, so the timer
72+
// and progress continue seamlessly with no jump.
73+
snapshotFlow { viewModel.stateFlow.value.processingProgress }
74+
.first { it.loading }
75+
76+
awaitingWallet = false
77+
}
78+
} else {
79+
LaunchedEffect(viewModel, swapId) {
80+
viewModel.dispatchEvent(Event.OnSwapIdChanged(swapId))
81+
}
4082
}
4183

4284
LaunchedEffect(viewModel) {
@@ -62,4 +104,4 @@ class TokenTxProcessingScreen(val swapId: SwapId) : ModalScreen, Parcelable {
62104
BackHandler { /* intercept */ }
63105
DisableSheetGestures()
64106
}
65-
}
107+
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ import com.getcode.view.LoadingSuccessState
4545

4646
@Composable
4747
internal fun TokenTxProcessingScreen(
48-
viewModel: BuySellSwapTokenViewModel
48+
viewModel: BuySellSwapTokenViewModel,
49+
processingProgressOverride: LoadingSuccessState? = null,
4950
) {
5051
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
51-
TokenTxProcessingScreen(state = state, dispatch = viewModel::dispatchEvent)
52+
val effectiveState = if (processingProgressOverride != null) {
53+
state.copy(processingProgress = processingProgressOverride)
54+
} else {
55+
state
56+
}
57+
TokenTxProcessingScreen(state = effectiveState, dispatch = viewModel::dispatchEvent)
5258
}
5359

5460
@Composable

apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import android.annotation.SuppressLint
55
import android.content.Context
66
import androidx.compose.runtime.Composable
77
import androidx.compose.runtime.LaunchedEffect
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.remember
811
import androidx.compose.runtime.rememberCoroutineScope
12+
import androidx.compose.runtime.setValue
913
import androidx.compose.ui.platform.LocalContext
1014
import androidx.compose.ui.platform.LocalUriHandler
1115
import androidx.lifecycle.Lifecycle
@@ -75,6 +79,9 @@ fun ExternalWalletOnRampHandler(
7579
}
7680
} ?: run { navigator.popAll() }
7781
}
82+
var preNavigatedToEntry by remember { mutableStateOf(false) }
83+
var preNavigatedToProcessing by remember { mutableStateOf(false) }
84+
7885
val uriHandler = LocalUriHandler.current
7986
val context = LocalContext.current
8087

@@ -167,6 +174,19 @@ fun ExternalWalletOnRampHandler(
167174
type = TraceType.Process
168175
)
169176
if (uri?.canNativelyHandle(context) == true) {
177+
val origin = state.origin
178+
if (origin is AppRoute.Token.Info) {
179+
preNavigatedToEntry = true
180+
navigator.push(
181+
ScreenRegistry.get(
182+
AppRoute.Token.SwapTransact(
183+
TokenSwapPurpose.FundWithWallet(origin.mint),
184+
forNeededFunds = origin.forNeededFunds
185+
)
186+
)
187+
)
188+
}
189+
170190
analytics.connectWallet(state.provider!!)
171191
uriHandler.openUri(uri.toString())
172192
state.deeplinkState = ExternalWalletState.CONNECTING
@@ -196,19 +216,24 @@ fun ExternalWalletOnRampHandler(
196216
message = "wallet connected",
197217
type = TraceType.Process
198218
)
199-
when (val origin = state.origin) {
200-
is AppRoute.Token.Info -> {
201-
navigator.push(
202-
ScreenRegistry.get(
203-
AppRoute.Token.SwapTransact(
204-
TokenSwapPurpose.FundWithWallet(origin.mint),
205-
forNeededFunds = origin.forNeededFunds
219+
if (preNavigatedToEntry) {
220+
// Already pushed SwapTransact at STARTED
221+
preNavigatedToEntry = false
222+
} else {
223+
when (val origin = state.origin) {
224+
is AppRoute.Token.Info -> {
225+
navigator.push(
226+
ScreenRegistry.get(
227+
AppRoute.Token.SwapTransact(
228+
TokenSwapPurpose.FundWithWallet(origin.mint),
229+
forNeededFunds = origin.forNeededFunds
230+
)
206231
)
207232
)
208-
)
209-
}
210-
else -> {
211-
navigator.push(ScreenRegistry.get(AppRoute.OnRamp.AmountEntry))
233+
}
234+
else -> {
235+
navigator.push(ScreenRegistry.get(AppRoute.OnRamp.AmountEntry))
236+
}
212237
}
213238
}
214239
}
@@ -226,6 +251,17 @@ fun ExternalWalletOnRampHandler(
226251
message = "wallet transact uri: $uri",
227252
type = TraceType.Process
228253
)
254+
255+
val swapId = state.swapId
256+
if (state.origin is AppRoute.Token.Info && swapId != null) {
257+
preNavigatedToProcessing = true
258+
navigator.push(
259+
ScreenRegistry.get(
260+
AppRoute.Token.TxProcessing(swapId, awaitExternalWallet = true)
261+
)
262+
)
263+
}
264+
229265
analytics.amountSelectedForWalletTransfer(state.provider!!, state.amount!!.underlyingTokenAmount)
230266
uriHandler.openUri(uri.toString())
231267
}
@@ -255,6 +291,12 @@ fun ExternalWalletOnRampHandler(
255291
)
256292
analytics.transactionSubmittedToWallet(state.provider!!)
257293

294+
if (preNavigatedToProcessing) {
295+
// TxProcessingScreen observes TRANSACTED, calls reset() and dispatches OnSwapIdChanged
296+
preNavigatedToProcessing = false
297+
return@LaunchedEffect
298+
}
299+
258300
val swapId = state.swapId
259301
state.reset()
260302

0 commit comments

Comments
 (0)