Skip to content

Commit 55c7310

Browse files
committed
fix(nav3): resolve nested navigation issues with sheets and deeplinks
- Fix Phantom deeplinks by introducing pendingNavigation on ExternalWalletDeeplinkState so inner sheet screens navigate via their local navigator instead of the root backstack - Skip general deeplink destination routing for external wallet returns to prevent replaceAll from destroying the sheet - Handle user cancellation in Phantom by detecting IDLE state on TxProcessingScreen and popping back - Fix duplicate bottom bar messages by limiting the messaging decorator to only the topmost non-sheet entry (using entry.metadata instead of the broken reified T.metadata() which always resolved as NavKey) - Fix ContextThemeWrapper cast crash in PermissionCheck inside ModalBottomSheet by using Context.getActivity() unwrapper - Support inner sheet navigation for deeplinks (email verification) by adding innerRoutes to Main.Sheet and packing post-sheet routes in navigateTo() - Extract blocking views (biometrics, update required) into NavBlockingOverlayEntryDecorator
1 parent 292d428 commit 55c7310

41 files changed

Lines changed: 1611 additions & 651 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.flipcash.app.internal.ui
22

3+
import androidx.compose.animation.EnterTransition
4+
import androidx.compose.animation.ExitTransition
5+
import androidx.compose.animation.slideInHorizontally
6+
import androidx.compose.animation.slideOutHorizontally
7+
import androidx.compose.animation.togetherWith
38
import androidx.compose.foundation.layout.Box
4-
import androidx.compose.foundation.layout.fillMaxSize
59
import androidx.compose.runtime.Composable
610
import androidx.compose.runtime.CompositionLocalProvider
711
import androidx.compose.runtime.LaunchedEffect
@@ -20,15 +24,18 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
2024
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2125
import androidx.navigation3.runtime.NavBackStack
2226
import androidx.navigation3.runtime.NavKey
27+
import androidx.navigation3.scene.OverlayScene
2328
import androidx.navigation3.scene.SinglePaneSceneStrategy
2429
import com.flipcash.app.analytics.rememberAnalytics
2530
import com.flipcash.app.android.BuildConfig
2631
import com.flipcash.app.bill.customization.BillPlaygroundScaffold
2732
import com.flipcash.app.core.LocalUserManager
2833
import com.flipcash.app.core.AppRoute
29-
import com.flipcash.app.core.navigation.DeeplinkType
34+
import com.flipcash.app.contact.verification.EmailVerificationFlow
35+
import com.flipcash.app.core.navigation.DeeplinkAction
3036
import com.flipcash.app.internal.ui.navigation.AppPreloads
3137
import com.flipcash.app.internal.ui.navigation.appEntryProvider
38+
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
3239
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
3340
import com.flipcash.app.onramp.ExternalWalletOnRampHandler
3441
import com.flipcash.app.onramp.LocalExternalWalletState
@@ -38,7 +45,6 @@ import com.flipcash.app.payments.PaymentScaffold
3845
import com.flipcash.app.router.LocalRouter
3946
import com.flipcash.app.session.LocalSessionController
4047
import com.flipcash.app.theme.FlipcashTheme
41-
import com.flipcash.app.updates.UpdateRequiredBlockingView
4248
import com.flipcash.features.shareapp.R
4349
import com.flipcash.services.user.AuthState
4450
import com.getcode.libs.biometrics.BiometricsError
@@ -53,10 +59,10 @@ import com.getcode.solana.rpc.RpcConfig
5359
import com.getcode.theme.CodeTheme
5460
import com.getcode.ui.biometrics.LocalBiometricsState
5561
import com.getcode.ui.biometrics.rememberBiometricsState
56-
import com.getcode.ui.biometrics.views.BiometricsBlockingView
5762
import com.getcode.ui.components.OnLifecycleEvent
5863
import com.getcode.ui.components.bars.rememberBarManager
5964
import com.getcode.ui.core.RestrictionType
65+
import com.flipcash.app.core.extensions.navigateTo
6066
import dev.bmcreations.tipkit.TipScaffold
6167
import dev.bmcreations.tipkit.engines.TipsEngine
6268
import dev.theolm.rinku.DeepLink
@@ -87,15 +93,9 @@ internal fun App(
8793
}
8894

8995
var deepLink by remember { mutableStateOf<DeepLink?>(null) }
90-
var loginRequest by remember { mutableStateOf<String?>(null) }
9196
val userManager = LocalUserManager.current!!
9297
DeepLinkListener {
9398
analytics.deeplinkOpened(it.data)
94-
val type = router.processType(it)
95-
analytics.deeplinkParsed(type, it.data)
96-
if (type is DeeplinkType.Login) {
97-
loginRequest = type.entropy
98-
}
9999
deepLink = it
100100
}
101101

@@ -145,8 +145,6 @@ internal fun App(
145145
state = externalWalletOnRamp,
146146
lifecycleOwner = LocalLifecycleOwner.current,
147147
navigator = codeNavigator,
148-
router = router,
149-
deepLink = deepLink,
150148
) {
151149
AppNavHost(
152150
navigator = codeNavigator,
@@ -155,7 +153,8 @@ internal fun App(
155153
rememberNavMessagingEntryDecorator(
156154
codeNavigator.backStack,
157155
barManager
158-
)
156+
),
157+
rememberNavBlockingOverlayEntryDecorator(),
159158
),
160159
sceneStrategy = ModalBottomSheetSceneStrategy<NavKey>(
161160
codeNavigator.resultStore
@@ -164,53 +163,90 @@ internal fun App(
164163
codeNavigator.backStack.lastIndex - 1
165164
)
166165
} then SinglePaneSceneStrategy(),
167-
onBack = { codeNavigator.navigateBack() },
168-
entryProvider = { key ->
169-
appEntryProvider(
170-
key = key,
171-
resultStateRegistry = resultStateRegistry,
172-
barManager = barManager,
173-
deepLink = { deepLink },
174-
)
166+
transitionSpec = {
167+
if (targetState is OverlayScene<*> || initialState is OverlayScene<*>) {
168+
EnterTransition.None togetherWith ExitTransition.None
169+
} else {
170+
slideInHorizontally(initialOffsetX = { it }) togetherWith
171+
slideOutHorizontally(targetOffsetX = { -it })
172+
}
173+
},
174+
popTransitionSpec = {
175+
if (targetState is OverlayScene<*> || initialState is OverlayScene<*>) {
176+
EnterTransition.None togetherWith ExitTransition.None
177+
} else {
178+
slideInHorizontally(initialOffsetX = { -it }) togetherWith
179+
slideOutHorizontally(targetOffsetX = { it })
180+
}
175181
},
182+
predictivePopTransitionSpec = {
183+
if (targetState is OverlayScene<*> || initialState is OverlayScene<*>) {
184+
EnterTransition.None togetherWith ExitTransition.None
185+
} else {
186+
slideInHorizontally(initialOffsetX = { -it }) togetherWith
187+
slideOutHorizontally(targetOffsetX = { it })
188+
}
189+
},
190+
onBack = { codeNavigator.navigateBack() },
191+
entryProvider = appEntryProvider(
192+
resultStateRegistry = resultStateRegistry,
193+
barManager = barManager,
194+
deepLink = { deepLink },
195+
),
176196
)
177197
}
178198

179199
LaunchedEffect(deepLink) {
180-
if (codeNavigator.currentRouteKey is AppRoute.Loading) return@LaunchedEffect
181-
if (deepLink != null) {
182-
val routes = router.processDestination(deepLink)
183-
if (routes.isNotEmpty()) {
184-
codeNavigator.replaceAll(routes)
185-
}
186-
deepLink = null
187-
}
188-
}
200+
val link = deepLink ?: return@LaunchedEffect
189201

190-
LaunchedEffect(
191-
loginRequest,
192-
codeNavigator.lastItem,
193-
userManager.authState
194-
) {
195-
if (codeNavigator.currentRouteKey is AppRoute.Loading) return@LaunchedEffect
196-
if (userManager.authState !is AuthState.LoggedInWithUser) {
197-
loginRequest = null
202+
if (codeNavigator.currentRouteKey is AppRoute.Loading) {
203+
// Cold start — MainRoot handles it via the deepLink lambda
198204
return@LaunchedEffect
199205
}
200-
loginRequest?.let { entropy ->
201-
viewModel.handleLoginEntropy(
202-
entropy,
206+
207+
when (val action = router.dispatch(link)) {
208+
is DeeplinkAction.Navigate -> {
209+
// If a verification code targets a screen already open,
210+
// deliver via side-channel and skip navigation.
211+
val verification = action.routes
212+
.filterIsInstance<AppRoute.Verification>()
213+
.firstOrNull()
214+
val email = verification?.email
215+
val code = verification?.emailVerificationCode
216+
val delivered = if (email != null && code != null) {
217+
EmailVerificationFlow.deliverCode(email, code)
218+
} else false
219+
220+
if (!delivered) {
221+
codeNavigator.navigateTo(action.routes)
222+
}
223+
}
224+
is DeeplinkAction.ExternalWallet -> externalWalletOnRamp.handleWalletDeeplink(action.type)
225+
is DeeplinkAction.Login -> viewModel.handleLoginEntropy(
226+
action.entropy,
203227
onSwitchAccount = {
204-
loginRequest = null
205228
codeNavigator.replaceAll(
206229
AppRoute.Onboarding.Login(
207-
entropy,
230+
action.entropy,
208231
fromDeeplink = true
209232
)
210233
)
211234
},
212-
onDismissed = { loginRequest = null }
235+
onDismissed = { }
213236
)
237+
is DeeplinkAction.OpenCashLink -> session.openCashLink(action.entropy)
238+
DeeplinkAction.None -> {}
239+
}
240+
deepLink = null
241+
}
242+
243+
LaunchedEffect(userState.authState) {
244+
if (userState.authState == AuthState.LoggedOut) {
245+
val current = codeNavigator.currentRouteKey
246+
if (current !is AppRoute.Loading && current !is AppRoute.Onboarding) {
247+
codeNavigator.pendingSheetDismiss = null
248+
codeNavigator.replaceAll(AppRoute.Onboarding.Login())
249+
}
214250
}
215251
}
216252

@@ -243,8 +279,6 @@ internal fun App(
243279
}
244280
}
245281

246-
BiometricsBlockingView(modifier = Modifier.fillMaxSize(), biometricsState)
247-
UpdateRequiredBlockingView(modifier = Modifier.fillMaxSize(), biometricsState = biometricsState)
248282
}
249283
}
250284
}

0 commit comments

Comments
 (0)