diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TileButton.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TileButton.kt
index fcffab2b3f..1f053a0cdc 100644
--- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TileButton.kt
+++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TileButton.kt
@@ -2,10 +2,13 @@ package com.flipcash.app.core.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
@@ -15,17 +18,35 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.getcode.theme.CodeTheme
import com.getcode.theme.extraSmall
-import androidx.compose.foundation.clickable
+
+/**
+ * Controls how a [TileButton] arranges its icon and label.
+ */
+enum class TileButtonStyle {
+ /** Icon centered above the label — the default compact tile. */
+ Centered,
+
+ /**
+ * Icon pinned to the top-start and the label to the bottom-start, spread across the
+ * height of the tile. Used for the larger call-to-action tiles.
+ */
+ Spread,
+}
@Composable
fun TileButton(
text: String,
icon: Painter,
modifier: Modifier = Modifier,
- contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.grid.x6),
+ style: TileButtonStyle = TileButtonStyle.Centered,
+ contentPadding: PaddingValues = when (style) {
+ TileButtonStyle.Centered -> PaddingValues(vertical = CodeTheme.dimens.grid.x6)
+ TileButtonStyle.Spread -> PaddingValues(CodeTheme.dimens.grid.x4)
+ },
onClick: () -> Unit,
) {
Box(
@@ -39,21 +60,52 @@ fun TileButton(
.padding(contentPadding),
contentAlignment = Alignment.Center,
) {
- Column(
- verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(
- modifier = Modifier.size(32.dp),
- painter = icon,
- contentDescription = null,
- colorFilter = ColorFilter.tint(CodeTheme.colors.textMain),
- )
- Text(
- text = text,
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.textMain,
- )
+ when (style) {
+ TileButtonStyle.Centered -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TileButtonIcon(icon)
+ Text(
+ text = text,
+ style = CodeTheme.typography.textMedium,
+ color = CodeTheme.colors.textMain,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+
+ TileButtonStyle.Spread -> {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.Start,
+ ) {
+ TileButtonIcon(icon, style)
+ Text(
+ text = text,
+ style = CodeTheme.typography.textMedium,
+ color = CodeTheme.colors.textMain,
+ )
+ }
+ }
}
}
-}
\ No newline at end of file
+}
+
+@Composable
+private fun TileButtonIcon(icon: Painter, style: TileButtonStyle = TileButtonStyle.Centered) {
+ val size = when (style) {
+ TileButtonStyle.Centered -> 32.dp
+ TileButtonStyle.Spread -> 24.dp
+ }
+ Image(
+ modifier = Modifier.size(size),
+ painter = icon,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(CodeTheme.colors.textMain),
+ )
+}
diff --git a/apps/flipcash/core/src/main/res/drawable/ic_globe.xml b/apps/flipcash/core/src/main/res/drawable/ic_globe.xml
new file mode 100644
index 0000000000..5723df3c42
--- /dev/null
+++ b/apps/flipcash/core/src/main/res/drawable/ic_globe.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt
index 739c489a66..23f79d2a84 100644
--- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt
+++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt
@@ -3,11 +3,13 @@ package com.flipcash.app.balance.internal
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.fillMaxWidth
@@ -33,9 +35,11 @@ import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.ui.AppreciationStyle
import com.flipcash.app.core.ui.TokenCardStack
import com.flipcash.app.balance.internal.components.BalanceHeader
-import com.flipcash.app.balance.internal.components.OnboardingFunnel
-import com.flipcash.app.balance.internal.components.OnboardingItem
+import com.flipcash.app.balance.internal.components.NewUserTutorial
+import com.flipcash.app.balance.internal.components.TutorialItem
import com.flipcash.app.core.navigation.LocalTabBarPadding
+import com.flipcash.app.core.ui.TileButton
+import com.flipcash.app.core.ui.TileButtonStyle
import com.flipcash.app.tokens.ui.SelectTokenViewModel
import com.flipcash.features.balance.R
import com.flipcash.shared.transactionhistory.ActivityFeedRow
@@ -80,7 +84,7 @@ internal fun WalletScreenContent(
start = CodeTheme.dimens.inset,
end = CodeTheme.dimens.inset,
bottom = LocalTabBarPadding.current.calculateBottomPadding() + CodeTheme.dimens.grid.x12,
- )
+ ),
) {
item {
// v2 wallet header: 96 dp top / 44 dp bottom per Figma node 8966:1578.
@@ -101,20 +105,20 @@ internal fun WalletScreenContent(
Spacer(Modifier.height(CodeTheme.dimens.grid.x6))
}
- if (!balanceState.isOnboardingComplete) {
+ if (!balanceState.isNewUserTutorialComplete) {
item {
- OnboardingFunnel(
+ NewUserTutorial(
modifier = Modifier.fillMaxWidth()
.padding(bottom = CodeTheme.dimens.grid.x5),
title = stringResource(R.string.title_tipOnboarding),
items = balanceState.onboardingItems,
) { item ->
when (item) {
- is OnboardingItem.AddMoney -> {
+ is TutorialItem.AddMoney -> {
dispatchEvent(WalletViewModel.Event.PresentDepositOptions)
}
- is OnboardingItem.ScanTipCard -> {
-
+ is TutorialItem.ScanTipCard -> {
+ dispatchEvent(WalletViewModel.Event.OpenScreen(AppRoute.Main.Scanner))
}
}
}
@@ -139,34 +143,6 @@ internal fun WalletScreenContent(
}
}
- if (balanceState.hasAddedMoney) {
- item {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(onClick = { dispatchEvent(WalletViewModel.Event.PresentDepositOptions) })
- .padding(vertical = CodeTheme.dimens.inset),
- contentAlignment = Alignment.Center,
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
- ) {
- Icon(
- painter = rememberVectorPainter(Icons.Outlined.AddCircleOutline),
- contentDescription = null,
- tint = CodeTheme.colors.textSecondary,
- )
- Text(
- text = stringResource(R.string.action_addMoney),
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.textSecondary,
- )
- }
- }
- }
- }
-
if (balanceState.transactions.isNotEmpty()) {
item(key = "recentHeader") {
// Tap the header to dive into the full paged activity history.
@@ -206,5 +182,44 @@ internal fun WalletScreenContent(
ActivityFeedRow(item = item, modifier = Modifier.fillMaxWidth())
}
}
+
+ if (balanceState.hasAddedMoney) {
+ item {
+ Spacer(Modifier.height(CodeTheme.dimens.grid.x6))
+ }
+
+ item {
+ // Equal-height tiles: the taller (wrapping) label drives the row height and the
+ // shorter tile stretches to match, so each tile can spread its icon/label vertically.
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min),
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
+ ) {
+ TileButton(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ style = TileButtonStyle.Spread,
+ text = stringResource(R.string.action_addMoney),
+ icon = rememberVectorPainter(Icons.Outlined.AddCircleOutline),
+ ) {
+ dispatchEvent(WalletViewModel.Event.PresentDepositOptions)
+ }
+
+ TileButton(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ style = TileButtonStyle.Spread,
+ text = stringResource(R.string.action_discoverCurrencies),
+ icon = painterResource(R.drawable.ic_globe),
+ ) {
+ dispatchEvent(WalletViewModel.Event.OpenScreen(AppRoute.Token.Discovery))
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt
index ce4bf80370..5e6967094f 100644
--- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt
+++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletViewModel.kt
@@ -3,7 +3,7 @@ package com.flipcash.app.balance.internal
import androidx.lifecycle.viewModelScope
import com.flipcash.app.analytics.Analytics
import com.flipcash.app.analytics.FlipcashAnalyticsService
-import com.flipcash.app.balance.internal.components.OnboardingItem
+import com.flipcash.app.balance.internal.components.TutorialItem
import com.flipcash.app.core.AppRoute
import com.flipcash.shared.transactionhistory.ActivityFeedCoordinator
import com.flipcash.shared.transactionhistory.TransactionListItem
@@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@@ -42,7 +41,7 @@ internal class WalletViewModel @Inject constructor(
) {
data class State(
val preferredOnRampProvider: OnRampProvider.Defined? = null,
- val onboardingItems: List = emptyList(),
+ val onboardingItems: List = emptyList(),
/**
* Preview of the most recent unified cross-token activity — at most [RECENT_PREVIEW_COUNT]
* rows. The coordinator owns the mapping and enforces the limit; the full paged history is a
@@ -51,14 +50,14 @@ internal class WalletViewModel @Inject constructor(
val transactions: List = emptyList(),
) {
val hasAddedMoney: Boolean
- get() = onboardingItems.find { it is OnboardingItem.AddMoney }?.isCompleted == true
+ get() = onboardingItems.find { it is TutorialItem.AddMoney }?.isCompleted == true
- val isOnboardingComplete: Boolean
+ val isNewUserTutorialComplete: Boolean
get() = onboardingItems.all { it.isCompleted }
}
sealed interface Event {
- data class OnOnboardingItemsUpdated(val items: List): Event
+ data class OnOnboardingItemsUpdated(val items: List): Event
data class OnTransactionsUpdated(val transactions: List) : Event
data class OnPreferredOnRampProviderChanged(val provider: OnRampProvider.Defined?) : Event
@@ -97,8 +96,8 @@ internal class WalletViewModel @Inject constructor(
chatCoordinator.hasEverTipped(),
) { hasAddedMoney, hasTipped ->
listOf(
- OnboardingItem.AddMoney(isCompleted = hasAddedMoney),
- OnboardingItem.ScanTipCard(isCompleted = hasTipped),
+ TutorialItem.AddMoney(isCompleted = hasAddedMoney),
+ TutorialItem.ScanTipCard(isCompleted = hasTipped),
)
}
.onEach { items -> dispatchEvent(Event.OnOnboardingItemsUpdated(items)) }
diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/OnboardingFunnel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt
similarity index 94%
rename from apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/OnboardingFunnel.kt
rename to apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt
index cb994e8dc8..c8a9f930cc 100644
--- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/OnboardingFunnel.kt
+++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/NewUserTutorial.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.util.fastForEach
import com.flipcash.features.balance.R
import com.getcode.theme.CodeTheme
-sealed interface OnboardingItem {
+sealed interface TutorialItem {
val title: String
@Composable get
val description: String
@@ -39,7 +39,7 @@ sealed interface OnboardingItem {
@Composable get
val isCompleted: Boolean
- class AddMoney(override val isCompleted: Boolean) : OnboardingItem {
+ class AddMoney(override val isCompleted: Boolean) : TutorialItem {
override val title: String
@Composable get() = stringResource(R.string.title_addMoney)
override val description: String
@@ -49,7 +49,7 @@ sealed interface OnboardingItem {
}
- class ScanTipCard(override val isCompleted: Boolean) : OnboardingItem {
+ class ScanTipCard(override val isCompleted: Boolean) : TutorialItem {
override val title: String
@Composable get() = stringResource(R.string.title_scanTipCard)
override val description: String
@@ -60,11 +60,11 @@ sealed interface OnboardingItem {
}
@Composable
-fun OnboardingFunnel(
+fun NewUserTutorial(
title: String,
- items: List,
+ items: List,
modifier: Modifier = Modifier,
- onItemClicked: (OnboardingItem) -> Unit,
+ onItemClicked: (TutorialItem) -> Unit,
) {
val completedCount = remember(items) { items.count { it.isCompleted } }
@@ -110,7 +110,7 @@ fun OnboardingFunnel(
@Composable
private fun OnboardingItemRow(
- item: OnboardingItem,
+ item: TutorialItem,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
diff --git a/apps/flipcash/features/discovery/build.gradle.kts b/apps/flipcash/features/discovery/build.gradle.kts
index 5875ff709c..5a38231e3f 100644
--- a/apps/flipcash/features/discovery/build.gradle.kts
+++ b/apps/flipcash/features/discovery/build.gradle.kts
@@ -7,6 +7,8 @@ android {
}
dependencies {
+ implementation(libs.bundles.haze)
+
implementation(project(":apps:flipcash:shared:analytics"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:shareable"))
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
index 72c68f4711..1cd54eaf41 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/TokenDiscoveryScreenContent.kt
@@ -61,20 +61,17 @@ private fun TokenDiscoveryScreenContent(
dispatch: (TokenDiscoveryViewModel.Event) -> Unit
) {
val listState = rememberLazyListState()
- CodeScaffold { padding ->
- AnimatedContent(
- targetState = state.tokens,
- transitionSpec = { fadeIn(tween()) togetherWith fadeOut(tween()) },
- contentKey = { it::class }, // only crossfade on type change, not data updates
- ) { tokens ->
- TokenLeaderboard(
- category = state.category,
- state = listState,
- tokens = tokens,
- padding = padding,
- dispatch = dispatch
- )
- }
+ AnimatedContent(
+ targetState = state.tokens,
+ transitionSpec = { fadeIn(tween()) togetherWith fadeOut(tween()) },
+ contentKey = { it::class }, // only crossfade on type change, not data updates
+ ) { tokens ->
+ TokenLeaderboard(
+ category = state.category,
+ state = listState,
+ tokens = tokens,
+ dispatch = dispatch
+ )
}
}
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
index b3bd39014a..20cffbec51 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenLeaderboard.kt
@@ -7,18 +7,21 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.FabPosition
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -27,10 +30,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.core.data.Loadable
import com.flipcash.app.core.data.isLoaded
import com.flipcash.app.discovery.internal.LeaderboardEntry
import com.flipcash.app.discovery.internal.TokenDiscoveryViewModel
+import com.flipcash.app.featureflags.FeatureFlag
+import com.flipcash.app.featureflags.LocalFeatureFlags
import com.flipcash.app.tokens.ui.CurrencyCreatorUpsellCard
import com.flipcash.features.discovery.R
import com.getcode.manager.BottomBarManager
@@ -41,88 +47,70 @@ import com.getcode.ui.core.unboundedClickable
import com.getcode.ui.core.verticalScrollStateGradient
import com.getcode.ui.theme.ButtonState
import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeScaffold
import com.getcode.ui.utils.sheetResignmentBehavior
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeSource
+import dev.chrisbanes.haze.rememberHazeState
@Composable
internal fun TokenLeaderboard(
category: DiscoverCategory?,
tokens: Loadable>,
- padding: PaddingValues,
state: LazyListState,
dispatch: (TokenDiscoveryViewModel.Event) -> Unit
) {
val reduceBottomPadding = CodeTheme.dimens.grid.x4
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .testTag("discovery_leaderboard")
- .verticalScrollStateGradient(
- state,
- color = CodeTheme.colors.background,
- isLongGradient = true,
- showAtEnd = false,
- )
- .addIf(tokens.isLoaded()) {
- Modifier.sheetResignmentBehavior(state)
- },
- state = state,
- contentPadding = PaddingValues(
- start = CodeTheme.dimens.inset,
- end = CodeTheme.dimens.inset,
- top = CodeTheme.dimens.grid.x2 + padding.calculateTopPadding(),
- bottom = (CodeTheme.dimens.grid.x2 + padding.calculateBottomPadding() - reduceBottomPadding).coerceAtLeast(
- 0.dp
- )
- )
- ) {
- when (tokens) {
- is Loadable.Error -> {
- item {
- Box(
- modifier = Modifier.fillParentMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(
- text = stringResource(R.string.title_discoverFailedToLoad),
- style = CodeTheme.typography.textLarge,
- color = CodeTheme.colors.textMain,
- textAlign = TextAlign.Center,
- )
- Text(
- text = tokens.message.orEmpty(),
- style = CodeTheme.typography.textSmall,
- color = CodeTheme.colors.textSecondary,
- textAlign = TextAlign.Center,
- )
-
- CodeButton(
- onClick = {
- dispatch(TokenDiscoveryViewModel.Event.Refresh)
- },
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(top = CodeTheme.dimens.grid.x2),
- contentPadding = PaddingValues(),
- text = stringResource(R.string.action_retry),
- shape = CircleShape,
- buttonState = ButtonState.Filled
- )
- }
- }
- }
- }
+ val features = LocalFeatureFlags.current
+ val isNewUi by features.observe(FeatureFlag.NewUi).collectAsStateWithLifecycle()
- is Loadable.Loaded -> {
- val results = tokens.data
- // currency creator upsell card
- item {
- CurrencyCreatorUpsellCard(modifier = Modifier.fillParentMaxWidth()) {
- dispatch(TokenDiscoveryViewModel.Event.CreateCurrency)
- }
- }
+ // The v2 upsell card sits over the list as the bottom bar; frost it against the leaderboard
+ // scrolling beneath. The inline (non-v2) card is part of the list itself, so it gets no hazeState.
+ val hazeState = rememberHazeState()
- if (results.isEmpty()) {
+ val currencyCreatorCard = @Composable { modifier: Modifier, haze: HazeState? ->
+ CurrencyCreatorUpsellCard(modifier = modifier, hazeState = haze) {
+ dispatch(TokenDiscoveryViewModel.Event.CreateCurrency)
+ }
+ }
+ CodeScaffold(
+ bottomBar = {
+ if (isNewUi) {
+ currencyCreatorCard(
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = CodeTheme.dimens.inset)
+ .navigationBarsPadding()
+ .padding(bottom = CodeTheme.dimens.grid.x3),
+ hazeState,
+ )
+ }
+ },
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .hazeSource(hazeState)
+ .testTag("discovery_leaderboard")
+ .verticalScrollStateGradient(
+ state,
+ color = CodeTheme.colors.background,
+ isLongGradient = true,
+ showAtEnd = isNewUi,
+ )
+ .addIf(tokens.isLoaded()) {
+ Modifier.sheetResignmentBehavior(state)
+ },
+ state = state,
+ contentPadding = PaddingValues(
+ start = CodeTheme.dimens.inset,
+ end = CodeTheme.dimens.inset,
+ top = CodeTheme.dimens.grid.x2,
+ bottom = (CodeTheme.dimens.grid.x2 + padding.calculateBottomPadding() - reduceBottomPadding).coerceAtLeast(0.dp)
+ )
+ ) {
+ when (tokens) {
+ is Loadable.Error -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
@@ -130,97 +118,142 @@ internal fun TokenLeaderboard(
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
- text = when (category) {
- DiscoverCategory.Popular -> stringResource(R.string.title_discoverEmptyPopular)
- DiscoverCategory.New -> stringResource(R.string.title_discoverEmptyNew)
- else -> ""
- },
+ text = stringResource(R.string.title_discoverFailedToLoad),
style = CodeTheme.typography.textLarge,
color = CodeTheme.colors.textMain,
textAlign = TextAlign.Center,
)
Text(
- modifier = Modifier.fillMaxWidth(0.6f),
- text = when (category) {
- DiscoverCategory.Popular -> stringResource(R.string.subtitle_discoverEmptyPopular)
- DiscoverCategory.New -> stringResource(R.string.subtitle_discoverEmptyNew)
- else -> ""
- },
- textAlign = TextAlign.Center,
+ text = tokens.message.orEmpty(),
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
+ textAlign = TextAlign.Center,
+ )
+
+ CodeButton(
+ onClick = {
+ dispatch(TokenDiscoveryViewModel.Event.Refresh)
+ },
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = CodeTheme.dimens.grid.x2),
+ contentPadding = PaddingValues(),
+ text = stringResource(R.string.action_retry),
+ shape = CircleShape,
+ buttonState = ButtonState.Filled
)
}
}
}
- } else {
- // leaderboard header
- item {
- Row(
- modifier = Modifier.padding(
- top = CodeTheme.dimens.inset,
- bottom = CodeTheme.dimens.grid.x1,
- ),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
- ) {
- Text(
- text = stringResource(R.string.title_leaderboard),
- style = CodeTheme.typography.textLarge,
- color = CodeTheme.colors.textMain,
- )
+ }
- Icon(
- modifier = Modifier
- .size(CodeTheme.dimens.staticGrid.x4)
- .unboundedClickable {
- dispatch(TokenDiscoveryViewModel.Event.LearnAboutLeaderboard)
- },
- imageVector = Icons.Outlined.Info,
- tint = CodeTheme.colors.textSecondary,
- contentDescription = stringResource(R.string.content_description_leaderboard),
- )
- }
+ is Loadable.Loaded -> {
+ val results = tokens.data
+ // currency creator upsell card
+ if (!isNewUi) {
+ item { currencyCreatorCard(Modifier.fillParentMaxWidth(), null) }
}
- itemsIndexed(
- items = tokens.data,
- key = { _, entry -> entry.key },
- contentType = { _, _ -> "token row" }
- ) { index, entry ->
- RankedTokenMetricsRow(
- modifier = Modifier.padding(
- vertical = CodeTheme.dimens.grid.x3,
- ),
- rank = index + 1,
- token = entry.token,
- ) {
- dispatch(TokenDiscoveryViewModel.Event.OpenTokenInfo(entry.token.address))
+ if (results.isEmpty()) {
+ item {
+ Box(
+ modifier = Modifier.fillParentMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = when (category) {
+ DiscoverCategory.Popular -> stringResource(R.string.title_discoverEmptyPopular)
+ DiscoverCategory.New -> stringResource(R.string.title_discoverEmptyNew)
+ else -> ""
+ },
+ style = CodeTheme.typography.textLarge,
+ color = CodeTheme.colors.textMain,
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ modifier = Modifier.fillMaxWidth(0.6f),
+ text = when (category) {
+ DiscoverCategory.Popular -> stringResource(R.string.subtitle_discoverEmptyPopular)
+ DiscoverCategory.New -> stringResource(R.string.subtitle_discoverEmptyNew)
+ else -> ""
+ },
+ textAlign = TextAlign.Center,
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary,
+ )
+ }
+ }
}
+ } else {
+ // leaderboard header
+ item {
+ Row(
+ modifier = Modifier.padding(
+ top = CodeTheme.dimens.inset,
+ bottom = CodeTheme.dimens.grid.x1,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
+ ) {
+ Text(
+ text = stringResource(R.string.title_leaderboard),
+ style = CodeTheme.typography.textLarge,
+ color = CodeTheme.colors.textMain,
+ )
- if (index < tokens.data.lastIndex) {
- HorizontalDivider(
- color = CodeTheme.colors.dividerVariant,
- modifier = Modifier
- .fillMaxWidth()
- )
+ Icon(
+ modifier = Modifier
+ .size(CodeTheme.dimens.staticGrid.x4)
+ .unboundedClickable {
+ dispatch(TokenDiscoveryViewModel.Event.LearnAboutLeaderboard)
+ },
+ imageVector = Icons.Outlined.Info,
+ tint = CodeTheme.colors.textSecondary,
+ contentDescription = stringResource(R.string.content_description_leaderboard),
+ )
+ }
+ }
+
+ itemsIndexed(
+ items = tokens.data,
+ key = { _, entry -> entry.key },
+ contentType = { _, _ -> "token row" }
+ ) { index, entry ->
+ RankedTokenMetricsRow(
+ modifier = Modifier.padding(
+ vertical = CodeTheme.dimens.grid.x3,
+ ),
+ rank = index + 1,
+ token = entry.token,
+ ) {
+ dispatch(TokenDiscoveryViewModel.Event.OpenTokenInfo(entry.token.address))
+ }
+ if (index < tokens.data.lastIndex) {
+ HorizontalDivider(
+ color = CodeTheme.colors.dividerVariant,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+
+ }
}
}
}
- }
- is Loadable.Loading -> {
- items(12) { index ->
- SkeletonRankedTokenMetricsRow(
- rank = index + 1,
- modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x3)
- )
- if (index < 7) {
- HorizontalDivider(
- color = CodeTheme.colors.dividerVariant,
- modifier = Modifier.fillMaxWidth()
+ is Loadable.Loading -> {
+ items(12) { index ->
+ SkeletonRankedTokenMetricsRow(
+ rank = index + 1,
+ modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x3)
)
+ if (index < 7) {
+ HorizontalDivider(
+ color = CodeTheme.colors.dividerVariant,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
}
}
}
diff --git a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenMetricsRow.kt b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenMetricsRow.kt
index 0e15a9e8bc..1f64ba245b 100644
--- a/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenMetricsRow.kt
+++ b/apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/internal/components/TokenMetricsRow.kt
@@ -36,12 +36,17 @@ import com.getcode.ui.core.addIf
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
+sealed interface RankingSystem {
+ object Holders : RankingSystem
+ object MarketCap : RankingSystem
+}
@Composable
internal fun RankedTokenMetricsRow(
rank: Int,
token: Token,
modifier: Modifier = Modifier,
+ rankingSystem: RankingSystem = RankingSystem.Holders,
onClick: () -> Unit,
) {
Row(
@@ -52,7 +57,7 @@ internal fun RankedTokenMetricsRow(
verticalAlignment = Alignment.CenterVertically
) {
RankBadge(rank)
- TokenMetricsRow(token = token, onClick = null)
+ TokenMetricsRow(token = token, rankingSystem = rankingSystem, onClick = null)
}
}
@@ -61,6 +66,67 @@ internal fun TokenMetricsRow(
token: Token,
modifier: Modifier = Modifier,
window: WindowedRange = WindowedRange.LastWeek,
+ rankingSystem: RankingSystem = RankingSystem.Holders,
+ onClick: (() -> Unit)? = null,
+) {
+ when (rankingSystem) {
+ RankingSystem.Holders -> {
+ val metricsDelta =
+ remember(token.holderMetrics) { token.holderMetrics.deltaForWindow(window) }
+ val change = if (metricsDelta >= 0) LineTrend.Up else LineTrend.Down
+ val deltaForWindow = buildString {
+ when (change) {
+ LineTrend.Down -> Unit // negative carried over from abbreviated
+ LineTrend.Up -> append("+")
+ }
+ append(metricsDelta.abbreviated())
+ append(" ")
+ append(
+ when (window) {
+ WindowedRange.AllTime -> stringResource(R.string.label_marketCapAllTime)
+ WindowedRange.LastDay -> stringResource(R.string.label_marketCapDay)
+ WindowedRange.LastWeek -> stringResource(R.string.label_marketCapWeek)
+ WindowedRange.LastMonth -> stringResource(R.string.label_marketCapMonth)
+ WindowedRange.LastYear -> stringResource(R.string.label_marketCapYear)
+ }
+ )
+ }
+
+ val subtitle = token.marketCap()?.formatted().orEmpty()
+
+ val value = pluralStringResource(
+ R.plurals.subtitle_personCount,
+ token.holderMetrics.currentHolders.toInt(),
+ token.holderMetrics.currentHolders.abbreviated()
+ )
+
+ TokenMetricsRow(
+ modifier = modifier,
+ token = token,
+ subtitle = subtitle,
+ value = value,
+ valueChange = deltaForWindow,
+ valueChangeColor = when (change) {
+ LineTrend.Down -> CodeTheme.colors.textSecondary
+ LineTrend.Up -> change.color
+ },
+ onClick = onClick,
+ )
+ }
+ RankingSystem.MarketCap -> {
+ // TODO: once we have market cap metrics cross window
+ }
+ }
+}
+
+@Composable
+private fun TokenMetricsRow(
+ token: Token,
+ subtitle: String,
+ value: String,
+ valueChange: String,
+ valueChangeColor: Color,
+ modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
) {
Row(
@@ -92,11 +158,7 @@ internal fun TokenMetricsRow(
)
Text(
modifier = Modifier.alignByBaseline(),
- text = pluralStringResource(
- R.plurals.subtitle_personCount,
- token.holderMetrics.currentHolders.toInt(),
- token.holderMetrics.currentHolders.abbreviated()
- ),
+ text = value,
color = CodeTheme.colors.textMain,
style = CodeTheme.typography.textMedium,
)
@@ -107,38 +169,15 @@ internal fun TokenMetricsRow(
) {
Text(
modifier = Modifier.alignByBaseline(),
- text = token.marketCap()?.formatted().orEmpty(),
+ text = subtitle,
color = CodeTheme.colors.textSecondary,
style = CodeTheme.typography.caption,
)
- val metricsDelta =
- remember(token.holderMetrics) { token.holderMetrics.deltaForWindow(window) }
- val change = if (metricsDelta >= 0) LineTrend.Up else LineTrend.Down
- val deltaForWindow = buildString {
- when (change) {
- LineTrend.Down -> Unit // negative carried over from abbreviated
- LineTrend.Up -> append("+")
- }
- append(metricsDelta.abbreviated())
- append(" ")
- append(
- when (window) {
- WindowedRange.AllTime -> stringResource(R.string.label_marketCapAllTime)
- WindowedRange.LastDay -> stringResource(R.string.label_marketCapDay)
- WindowedRange.LastWeek -> stringResource(R.string.label_marketCapWeek)
- WindowedRange.LastMonth -> stringResource(R.string.label_marketCapMonth)
- WindowedRange.LastYear -> stringResource(R.string.label_marketCapYear)
- }
- )
- }
Text(
modifier = Modifier.alignByBaseline(),
- text = deltaForWindow,
- color = when (change) {
- LineTrend.Down -> CodeTheme.colors.textSecondary
- LineTrend.Up -> change.color
- },
+ text = valueChange,
+ color = valueChangeColor,
style = CodeTheme.typography.caption,
)
}
diff --git a/apps/flipcash/shared/tokens/build.gradle.kts b/apps/flipcash/shared/tokens/build.gradle.kts
index c13c49b28e..471951ac69 100644
--- a/apps/flipcash/shared/tokens/build.gradle.kts
+++ b/apps/flipcash/shared/tokens/build.gradle.kts
@@ -18,6 +18,8 @@ dependencies {
implementation(libs.androidx.lifecycle.process)
+ implementation(libs.bundles.haze)
+
implementation(project(":apps:flipcash:shared:amount-entry"))
implementation(project(":apps:flipcash:shared:funding"))
implementation(project(":apps:flipcash:shared:transaction-history"))
diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/CurrencyCreatorUpsellCard.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/CurrencyCreatorUpsellCard.kt
index 19f74d7267..0259132827 100644
--- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/CurrencyCreatorUpsellCard.kt
+++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/CurrencyCreatorUpsellCard.kt
@@ -1,6 +1,7 @@
package com.flipcash.app.tokens.ui
import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -16,6 +17,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -27,17 +31,45 @@ import com.flipcash.app.theme.FlipcashThemeWrapper
import com.flipcash.app.theme.MultiDevicePreview
import com.flipcash.shared.tokens.R
import com.getcode.theme.CodeTheme
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.blur.HazeBlurStyle
+import dev.chrisbanes.haze.blur.HazeColorEffect
+import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.hazeEffect
@Composable
fun CurrencyCreatorUpsellCard(
modifier: Modifier = Modifier,
+ hazeState: HazeState? = null,
onClick: () -> Unit,
) {
+ val shape = CodeTheme.shapes.medium
+
+ // When a HazeState is supplied the card frosts whatever list content scrolls beneath it (iOS
+ // "liquid glass"), matching the v2 navigation pill: a wide blur plus a strong tint toward the
+ // BACKGROUND colour at high alpha, finished with a faint bright rim. `clip` must precede
+ // `hazeEffect` so the blur is bounded to the rounded card, not its bounding box. Falls back to the
+ // opaque surface when no HazeState is supplied (e.g. when the card is itself part of the list).
+ val glassTint = lerp(CodeTheme.colors.background, Color.White, 0.18f)
+ val liquidGlass = HazeBlurStyle(
+ blurRadius = 32.dp,
+ backgroundColor = CodeTheme.colors.background,
+ colorEffect = HazeColorEffect.tint(glassTint.copy(alpha = 0.72f)),
+ )
+ val glassBackground = if (hazeState != null) {
+ Modifier
+ .clip(shape)
+ .hazeEffect(hazeState) { blurEffect { style = liquidGlass } }
+ .border(CodeTheme.dimens.border, Color.White.copy(alpha = 0.08f), shape)
+ } else {
+ Modifier
+ }
+
Surface(
- modifier = modifier,
- color = CodeTheme.colors.surfaceVariant,
+ modifier = modifier.then(glassBackground),
+ color = if (hazeState != null) Color.Transparent else CodeTheme.colors.surfaceVariant,
contentColor = CodeTheme.colors.textMain,
- shape = CodeTheme.shapes.medium,
+ shape = shape,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
onClick = onClick
diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt
index 4b36bae688..0526beeb66 100644
--- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt
+++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SelectTokenViewModel.kt
@@ -137,11 +137,7 @@ class SelectTokenViewModel @Inject constructor(
appreciation = appreciation,
displayName = when (purpose) {
TokenPurpose.Balance -> {
- if (it.token.address == Mint.usdf && featureFlags.get(FeatureFlag.NewUi)) {
- resources.getString(R.string.displayName_dollars)
- } else {
- it.token.name
- }
+ it.token.name
}
is TokenPurpose.Swap,
@@ -151,7 +147,7 @@ class SelectTokenViewModel @Inject constructor(
TokenPurpose.Withdraw -> {
if (it.token.address == Mint.usdf) {
if (featureFlags.get(FeatureFlag.NewUi)) {
- resources.getString(R.string.displayName_dollars)
+ it.token.name
} else {
resources.getString(R.string.displayName_usdf)
}