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
17 changes: 6 additions & 11 deletions apps/flipcash/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,12 @@
android:scheme="https" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="app.flipcash.com"
android:pathPattern="/chat/.*"
android:scheme="https" />
</intent-filter>
<!--
No `/chat/.*` App Link. AppRouter deliberately does not classify chat links
(the direct-send entry point they opened was removed), so claiming the URL
here would capture it from the browser and dead-end the user on the home
screen. Restore this filter only alongside routing in AppRouter.
-->

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
Expand All @@ -33,6 +34,7 @@ import androidx.navigation3.scene.SinglePaneSceneStrategy
import com.flipcash.app.analytics.rememberAnalytics
import com.flipcash.app.android.BuildConfig
import com.flipcash.app.bill.customization.BillPlaygroundScaffold
import com.flipcash.app.cardexpand.CardExpansionController
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.LocalUserManager
import com.flipcash.app.core.extensions.navigateAll
Expand Down Expand Up @@ -118,6 +120,26 @@ internal fun App(

val isNewUi by features.observe(FeatureFlag.NewUi).collectAsStateWithLifecycle()

// Card-expand (iOS #587) is owned HERE rather than inside NewAppContent because the deeplink
// handling below sits outside the v1/v2 shells: a `/token` link opens the wallet's expanded card
// (see DeeplinkAction.OpenToken), which needs the controller. NewAppContent provides it to the
// tree; v1 has no expansion and never touches it.
val context = LocalContext.current
val cardExpansion = remember(context) {
CardExpansionController().apply {
// Feed the controller the user's real animation-scale so the expand honours "animations
// off" (accessibility/battery) yet isn't fooled by a stale ambient MotionDurationScale.
val resolver = context.contentResolver
animationScale = {
android.provider.Settings.Global.getFloat(
resolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
}
}
}

FlipcashTheme {
rememberQrBitmapPainter(
content = stringResource(
Expand Down Expand Up @@ -162,10 +184,16 @@ internal fun App(
codeNavigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
barManager = barManager,
cardExpansion = cardExpansion,
deepLink = { deepLink },
onPendingAction = { action ->
deeplinkHandled = true
when (action) {
// v2 cold start: the wallet is already the
// launch home, so the token just opens as its
// expanded card on top of it.
is DeeplinkAction.OpenToken ->
cardExpansion.beginExpanded(action.mint)
is DeeplinkAction.OpenCashLink ->
session.openCashLink(action.entropy)
is DeeplinkAction.PresentTipCard ->
Expand Down Expand Up @@ -257,6 +285,28 @@ internal fun App(
} else false

if (!delivered) {
codeNavigator.navigateAll(
action.routes,
isNewUi = isNewUi,
)
}
}

is DeeplinkAction.OpenToken -> {
if (isNewUi) {
// Land on the wallet tab (clearing anything pushed on
// it) and open the token as its EXPANDED CARD — the
// same overlay, chrome and dismissal a tap on the card
// gives. Pushing it instead reads as a modal on a stack
// the user never navigated. Mirrors iOS
// DeepLinkController's requestedCardMint.
codeNavigator.navigateAll(
listOf(AppRoute.Sheets.Wallet),
isNewUi = true,
)
cardExpansion.beginExpanded(action.mint)
} else {
// v1 has no card expansion — open the wallet sheet.
codeNavigator.navigateAll(action.routes)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -166,6 +165,7 @@ internal fun NewAppContent(
codeNavigator: CodeNavigator,
resultStateRegistry: NavResultStateRegistry,
barManager: BarManager,
cardExpansion: CardExpansionController,
deepLink: () -> DeepLink?,
onPendingAction: (DeeplinkAction) -> Unit = {},
) {
Expand All @@ -179,23 +179,10 @@ internal fun NewAppContent(
val tabBarHeight = remember { mutableStateOf(0.dp) }

// Card-expand (iOS #587): the wallet requests an expansion (via LocalCardExpansion); the detail is
// drawn HERE as an overlay above the nav content, driven by one progress scalar, so the deck stays
// drawn by CardExpandHost inside the wallet entry, driven by one progress scalar, so the deck stays
// composed and reorganises behind it. See CardExpansionController / CurrencyInfoExpansion.
val context = LocalContext.current
val cardExpansion = remember(context) {
CardExpansionController().apply {
// Feed the controller the user's real animation-scale so the expand honours "animations
// off" (accessibility/battery) yet isn't fooled by a stale ambient MotionDurationScale.
val resolver = context.contentResolver
animationScale = {
android.provider.Settings.Global.getFloat(
resolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
}
}
}
// [cardExpansion] is owned by App so a `/token` deeplink — which is handled there, outside this
// shell — can open a token as its expanded card instead of pushing a screen.
CompositionLocalProvider(LocalCardExpansion provides cardExpansion) {
Box(modifier = Modifier.fillMaxSize()) {
// Mark the nav content as the haze source so the frosted bar blurs whatever scrolls beneath it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package com.flipcash.app.internal.ui.navigation
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import com.flipcash.app.cardexpand.CardExpansionController
import com.flipcash.app.cardexpand.LocalCardExpansion
import com.flipcash.app.tokens.CurrencyInfoExpansion
import com.getcode.solana.keys.Mint
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

/**
Expand Down Expand Up @@ -38,6 +42,19 @@ internal fun CardExpandHost(content: @Composable () -> Unit) {
}
}

// A source-less expansion (a deeplink — CardExpansionController.beginExpanded) has no deck card to
// fly from, so there is nothing to animate: it lands already open, like iOS's
// `WalletScreen.openCardImmediately`. Wait for the overlay's hero slot to report its frame before
// snapping, so the first fully-visible frame already has its card in place rather than an empty slot.
LaunchedEffect(controller.expandedKey) {
val key = controller.expandedKey ?: return@LaunchedEffect
if (controller.sourceBounds != null) return@LaunchedEffect
snapshotFlow { controller.heroBounds }.filterNotNull().first()
if (controller.expandedKey === key) {
controller.snapTo(1f)
}
}

Box(modifier = Modifier) {
content()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.flipcash.app.core.LocalUserManager
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.extensions.navigateAll
import com.flipcash.app.core.extensions.resolveRoutes
import com.flipcash.app.core.extensions.resolveBackStack
import com.flipcash.app.featureflags.LocalFeatureFlags
import com.flipcash.app.router.LocalRouter
import com.flipcash.app.router.Router
Expand Down Expand Up @@ -132,7 +132,7 @@ internal fun MainRoot(
if (!current.startsWith(target)) {
navigator.replaceAll(launch.baseRoutes)
if (launch.deeplinkRoutes.isNotEmpty()) {
navigator.navigateAll(launch.deeplinkRoutes)
navigator.navigateAll(launch.deeplinkRoutes, isNewUi = isNewUi)
}
}

Expand All @@ -158,16 +158,17 @@ internal data class LaunchNavGraph(
val baseRoutes: List<NavKey>,
val deeplinkRoutes: List<AppRoute> = emptyList(),
val pendingAction: DeeplinkAction? = null,
/** v2 (tab-centric) shell. Changes how [deeplinkRoutes] resolve — see [resolveBackStack]. */
val isNewUi: Boolean = false,
) {
/**
* Predict the final backstack that [baseRoutes] + [navigateTo(deeplinkRoutes)] will produce.
* Uses the shared [resolveRoutes] to apply the same sheet-wrapping as [navigateTo]
* so we can compare against the current backstack and skip redundant navigation.
* Predict the final backstack that [baseRoutes] + `navigateAll(deeplinkRoutes)` will produce.
* Uses the shared [resolveBackStack] so it applies the same sheet-wrapping (v1) or tab-switch
* (v2) as `navigateAll`, letting us compare against the current backstack and skip redundant
* navigation.
*/
fun resolvedBackStack(): List<NavKey> {
if (deeplinkRoutes.isEmpty()) return baseRoutes
return baseRoutes + resolveRoutes(deeplinkRoutes)
}
fun resolvedBackStack(): List<NavKey> =
resolveBackStack(baseRoutes, deeplinkRoutes, isNewUi)
}

/**
Expand Down Expand Up @@ -225,19 +226,38 @@ internal fun buildNavGraphForLaunch(
is DeeplinkAction.Navigate -> LaunchNavGraph(
baseRoutes = listOf(home),
deeplinkRoutes = action.routes,
isNewUi = isNewUi,
)

// v2 opens a token link as the wallet's expanded card (a pending action applied
// on top of the wallet home, which is already the launch base); v1 has no card
// expansion, so it takes the route form — the wallet sheet with token info inside.
is DeeplinkAction.OpenToken -> if (isNewUi) {
LaunchNavGraph(
baseRoutes = listOf(home),
pendingAction = action,
isNewUi = true,
)
} else {
LaunchNavGraph(
baseRoutes = listOf(home),
deeplinkRoutes = action.routes,
isNewUi = false,
)
}

is DeeplinkAction.OpenCashLink,
is DeeplinkAction.PresentTipCard,
is DeeplinkAction.Login -> LaunchNavGraph(
baseRoutes = listOf(home),
pendingAction = action,
isNewUi = isNewUi,
)

else -> LaunchNavGraph(listOf(home))
else -> LaunchNavGraph(listOf(home), isNewUi = isNewUi)
}
} else {
LaunchNavGraph(listOf(home))
LaunchNavGraph(listOf(home), isNewUi = isNewUi)
}
}

Expand Down
Loading
Loading