diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt
index 373e84b9d6..ed9a64f29a 100644
--- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt
+++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt
@@ -11,8 +11,10 @@ import com.flipcash.app.core.AppRoute
fun NavBarButton.destinationRoute(): AppRoute? = when (this) {
NavBarButton.Scanner -> AppRoute.Main.Scanner
NavBarButton.Wallet -> AppRoute.Sheets.Wallet
- NavBarButton.Chats -> null // TODO(v2): wire the chats destination
- NavBarButton.TipCard -> null // TODO(v2): wire the tip-card destination
+ // Both tabs route through the tipping flow, seeded at the right step: the chats list vs the tip
+ // card (resumed = true lands the flow on TipCard alone). Deeplinks/decorators use the same route.
+ NavBarButton.Chats -> AppRoute.Sheets.Tips(resumed = false)
+ NavBarButton.TipCard -> AppRoute.Sheets.Tips(resumed = true)
// v1-only buttons never appear in the v2 bar.
NavBarButton.Give, NavBarButton.Discover, NavBarButton.Tips -> null
}
@@ -21,5 +23,7 @@ fun NavBarButton.destinationRoute(): AppRoute? = when (this) {
fun AppRoute.asNavBarTab(): NavBarButton? = when (this) {
AppRoute.Main.Scanner -> NavBarButton.Scanner
AppRoute.Sheets.Wallet -> NavBarButton.Wallet
+ // The tipping flow is home to both tabs; `resumed` distinguishes the tip card from the list.
+ is AppRoute.Sheets.Tips -> if (resumed) NavBarButton.TipCard else NavBarButton.Chats
else -> null
}
diff --git a/apps/flipcash/features/scanner/src/main/res/drawable/ic_home_options.xml b/apps/flipcash/core/src/main/res/drawable/ic_home_options.xml
similarity index 100%
rename from apps/flipcash/features/scanner/src/main/res/drawable/ic_home_options.xml
rename to apps/flipcash/core/src/main/res/drawable/ic_home_options.xml
diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index 86417e48b7..3e61855293 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -872,6 +872,7 @@
Tips
Tips
+ Chats
Receive Tips From Everyone
Add your name to receive tips
Start Receiving Tips
diff --git a/apps/flipcash/features/tipping/build.gradle.kts b/apps/flipcash/features/tipping/build.gradle.kts
index c32bde610b..e120534b9b 100644
--- a/apps/flipcash/features/tipping/build.gradle.kts
+++ b/apps/flipcash/features/tipping/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
implementation(project(":libs:messaging"))
implementation(project(":apps:flipcash:shared:amount-entry"))
implementation(project(":apps:flipcash:shared:bills"))
+ implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:chat"))
implementation(project(":apps:flipcash:shared:chat-ui"))
implementation(project(":apps:flipcash:shared:shareable"))
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt
new file mode 100644
index 0000000000..f4dcbc2fa2
--- /dev/null
+++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt
@@ -0,0 +1,172 @@
+package com.flipcash.app.tipping
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.flipcash.app.bills.ScannableRenderer
+import com.flipcash.app.bills.components.cards.LocalTipCardBaseAlpha
+import com.flipcash.app.bills.components.cards.LocalTipCardColor
+import com.flipcash.app.core.AppRoute
+import com.flipcash.app.core.bill.Scannable
+import com.flipcash.app.core.extensions.openAsSheet
+import com.flipcash.app.core.tipping.TipResult
+import com.flipcash.app.core.tipping.TipStep
+import com.flipcash.app.featureflags.FeatureFlag
+import com.flipcash.app.featureflags.LocalFeatureFlags
+import com.flipcash.app.tipping.internal.TipFlowViewModel
+import com.flipcash.app.tipping.internal.TipFlowViewModel.Event
+import com.flipcash.features.tipping.R
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.navigation.flow.flowSharedViewModel
+import com.getcode.navigation.flow.rememberFlowNavigator
+import com.getcode.theme.CodeTheme
+import com.getcode.ui.components.AppBarWithTitle
+import com.getcode.ui.components.CircularIconButton
+import com.getcode.ui.core.unboundedClickable
+import com.getcode.ui.theme.CodeScaffold
+
+/**
+ * The user's own tip card — always a step in the tipping [TippingFlowScreen] flow, so it shares the
+ * flow's [TipFlowViewModel]. Only the chrome differs by [FeatureFlag.NewUi]:
+ * - **v2**: a root tab (the flow seeded at TipCard) — the card centered, with the app menu reachable
+ * via the hamburger in the top-right.
+ * - **v1**: a sheet step — title bar with back, and a Share action.
+ */
+@Composable
+fun TipCardScreen() {
+ val features = LocalFeatureFlags.current
+ val isNewUi = remember(features) { features.observe(FeatureFlag.NewUi).value }
+
+ val viewModel = flowSharedViewModel()
+ val state by viewModel.stateFlow.collectAsStateWithLifecycle()
+
+ if (isNewUi) {
+ val navigator = LocalCodeNavigator.current
+ Box(modifier = Modifier.fillMaxSize()) {
+ TipCardArt(card = state.tipCard, modifier = Modifier.fillMaxSize())
+
+ Image(
+ painter = painterResource(R.drawable.ic_home_options),
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .statusBarsPadding()
+ .padding(vertical = CodeTheme.dimens.grid.x2)
+ .padding(horizontal = CodeTheme.dimens.grid.x3)
+ .clip(CircleShape)
+ .unboundedClickable { navigator.openAsSheet(AppRoute.Sheets.Menu) },
+ )
+ }
+ } else {
+ val flowNavigator = rememberFlowNavigator()
+ CodeScaffold(
+ topBar = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x10),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AppBarWithTitle(
+ title = stringResource(R.string.title_myTipCard),
+ titleAlignment = Alignment.CenterHorizontally,
+ onBackIconClicked = { flowNavigator.back() },
+ )
+ Text(
+ text = stringResource(R.string.subtitle_myTipCard),
+ style = CodeTheme.typography.textLarge,
+ color = CodeTheme.colors.textMain,
+ )
+ }
+ },
+ bottomBar = {
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(bottom = CodeTheme.dimens.grid.x3),
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x5, Alignment.CenterHorizontally),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(
+ modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x3),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3),
+ ) {
+ CircularIconButton(
+ imageSize = CodeTheme.dimens.staticGrid.x6,
+ buttonSize = CodeTheme.dimens.grid.x12,
+ onClick = {
+ viewModel.dispatchEvent(Event.ShareTipCard)
+ }
+ ) { size ->
+ Icon(
+ painter = painterResource(R.drawable.ic_remote_send),
+ contentDescription = null,
+ tint = Color.White,
+ // The send glyph is bottom-heavy (wide tray below a thin arrow),
+ // so geometric centering leaves it sitting visually low in the
+ // circle. Nudge it up slightly to optically center it.
+ modifier = Modifier
+ .requiredSize(size)
+ .offset(y = (-1.5).dp),
+ )
+ }
+ Text(
+ text = stringResource(R.string.action_share),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary,
+ )
+ }
+ }
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ TipCardArt(card = state.tipCard)
+ }
+ }
+ }
+}
+
+@Composable
+private fun TipCardArt(card: Scannable.TipCard?, modifier: Modifier = Modifier) {
+ if (card == null) return
+ CompositionLocalProvider(
+ // Static display (no camera behind the card), so render it opaque at the design's flattened
+ // colour rather than the translucent frosted fill — the 36% alpha otherwise multiplies the
+ // app's Brand background and reads incorrectly. Figma flattens the card to rgb(16,16,17).
+ LocalTipCardColor provides Color(0xFF101011),
+ LocalTipCardBaseAlpha provides 1f,
+ ) {
+ Box(modifier = modifier, contentAlignment = Alignment.Center) {
+ ScannableRenderer(scannable = card)
+ }
+ }
+}
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt
index 8d41ee8fa8..083999085e 100644
--- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt
+++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt
@@ -12,9 +12,7 @@ import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.tipping.TipResult
import com.flipcash.app.core.tipping.TipStep
import com.flipcash.app.tipping.internal.TipFlowViewModel
-import com.flipcash.app.tipping.internal.screens.TipCardScreen
import com.flipcash.app.tipping.internal.screens.TipInfoScreen
-import com.flipcash.app.tipping.internal.screens.TipsScreen
import com.getcode.navigation.annotatedEntry
import com.getcode.navigation.flow.FlowExitReason
import com.getcode.navigation.flow.FlowHost
@@ -38,8 +36,13 @@ fun TippingFlowScreen(
viewModel.dispatchEvent(TipFlowViewModel.Event.OnResumed(route.resumed))
}
+ // `resumed` is deterministically the tip card, so seed it synchronously rather than waiting for
+ // the VM's async steps to resolve. Otherwise the tip-card tab renders the default first step
+ // (the chats list) for a frame — briefly visible when a tab switch crossfades into it.
+ val steps = if (route.resumed) listOf(TipStep.TipCard) else state.steps
+
FlowHost(
- steps = state.steps,
+ steps = steps,
resumeAt = 0,
resultStateRegistry = resultStateRegistry,
// Proceeding off the end (TipCard's Done, or backing out at root) completes the flow.
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt
new file mode 100644
index 0000000000..4a3a204bf1
--- /dev/null
+++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt
@@ -0,0 +1,118 @@
+package com.flipcash.app.tipping
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.flipcash.app.core.AppRoute
+import com.flipcash.app.core.chat.ChatIdentifier
+import com.flipcash.app.core.tipping.TipStep
+import com.flipcash.app.featureflags.FeatureFlag
+import com.flipcash.app.featureflags.LocalFeatureFlags
+import com.flipcash.app.tipping.internal.TipFlowViewModel
+import com.flipcash.app.tipping.internal.components.TipChatRow
+import com.flipcash.features.tipping.R
+import com.flipcash.shared.chat.ui.ConversationReference
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.navigation.flow.flowSharedViewModel
+import com.getcode.theme.CodeTheme
+import com.getcode.ui.components.AppBarDefaults
+import com.getcode.ui.components.AppBarWithTitle
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeScaffold
+
+/**
+ * The tip DM conversation list — always a step in the tipping [TippingFlowScreen] flow, so it shares
+ * the flow's [TipFlowViewModel]. Only the chrome differs by [FeatureFlag.NewUi]:
+ * - **v2**: the "Chats" root tab — a flush large title over the list, no dismiss affordance (the
+ * root nav bar is the chrome); the tip card lives on its own tab.
+ * - **v1**: the "Tips" sheet step — centered title with a Close, and a sticky "Show Tip Card" button
+ * that advances the flow.
+ */
+@Composable
+fun TipsScreen() {
+ val features = LocalFeatureFlags.current
+ val isNewUi = remember(features) { features.observe(FeatureFlag.NewUi).value }
+
+ val viewModel = flowSharedViewModel()
+ val state by viewModel.stateFlow.collectAsStateWithLifecycle()
+ val navigator = LocalCodeNavigator.current
+
+ CodeScaffold(
+ topBar = {
+ if (isNewUi) {
+ AppBarWithTitle(
+ title = stringResource(R.string.title_chats),
+ titleTextStyle = CodeTheme.typography.screenTitleLarge,
+ )
+ } else {
+ AppBarWithTitle(
+ title = stringResource(R.string.title_tips),
+ titleAlignment = Alignment.CenterHorizontally,
+ endContent = {
+ AppBarDefaults.Close { navigator.hide() }
+ }
+ )
+ }
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ ) {
+ // v1 surfaces the tip card via a button here; v2 has a dedicated tip-card tab instead.
+ if (!isNewUi) {
+ stickyHeader {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = CodeTheme.dimens.inset)
+ .background(CodeTheme.colors.background),
+ contentAlignment = Alignment.Center
+ ) {
+ CodeButton(
+ modifier = Modifier.fillMaxWidth()
+ .padding(horizontal = CodeTheme.dimens.inset),
+ text = stringResource(R.string.action_showTipCard),
+ buttonState = ButtonState.Filled,
+ onClick = {
+ navigator.navigate(TipStep.TipCard)
+ },
+ )
+ }
+ }
+ }
+
+ tipChatItems(state.tipChats) { chat ->
+ navigator.push(AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(chat.chatId)))
+ }
+ }
+ }
+}
+
+private fun LazyListScope.tipChatItems(
+ chats: List,
+ onClick: (ConversationReference) -> Unit,
+) {
+ itemsIndexed(chats) { index, chat ->
+ TipChatRow(
+ chat = chat,
+ showDivider = index < chats.lastIndex,
+ ) {
+ onClick(chat)
+ }
+ }
+}
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/components/TipChatRow.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/components/TipChatRow.kt
new file mode 100644
index 0000000000..ba7fcebf7c
--- /dev/null
+++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/components/TipChatRow.kt
@@ -0,0 +1,62 @@
+package com.flipcash.app.tipping.internal.components
+
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import com.flipcash.shared.chat.ui.ChatListRow
+import com.flipcash.shared.chat.ui.ChatRowSubtitle
+import com.flipcash.shared.chat.ui.ChatRowTrailing
+import com.flipcash.shared.chat.ui.ConversationReference
+import com.flipcash.shared.chat.ui.SubtitleText
+import com.flipcash.shared.common.ui.ContactAvatar
+import com.getcode.theme.CodeTheme
+
+
+@Composable
+internal fun TipChatRow(
+ chat: ConversationReference,
+ modifier: Modifier = Modifier,
+ showDivider: Boolean = true,
+ onClick: () -> Unit,
+) {
+ ChatListRow(
+ modifier = modifier,
+ avatar = {
+ ContactAvatar(
+ image = chat.image,
+ displayName = chat.displayName.orEmpty(),
+ modifier = Modifier
+ .requiredSize(CodeTheme.dimens.staticGrid.x8)
+ .clip(CircleShape),
+ )
+ },
+ title = {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = chat.displayName.orEmpty(),
+ style = CodeTheme.typography.textMedium,
+ color = CodeTheme.colors.textMain,
+ )
+
+ ChatRowTrailing(
+ lastActivity = chat.lastActivity,
+ unreadCount = chat.unreadCount,
+ canOpen = true,
+ )
+ },
+ subtitle = {
+ ChatRowSubtitle(
+ isTyping = chat.isTyping,
+ preview = chat.lastMessagePreview,
+ fallback = {
+ SubtitleText("")
+ }
+ )
+ },
+ showDivider = showDivider,
+ onClick = onClick,
+ )
+}
\ No newline at end of file
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt
deleted file mode 100644
index 227c48e784..0000000000
--- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.flipcash.app.tipping.internal.screens
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-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.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.flipcash.app.bills.ScannableRenderer
-import com.flipcash.app.bills.components.cards.LocalTipCardBaseAlpha
-import com.flipcash.app.bills.components.cards.LocalTipCardColor
-import com.flipcash.app.core.tipping.TipResult
-import com.flipcash.app.core.tipping.TipStep
-import com.flipcash.app.tipping.internal.TipFlowViewModel
-import com.flipcash.app.tipping.internal.TipFlowViewModel.Event
-import com.flipcash.features.tipping.R
-import com.getcode.navigation.flow.flowSharedViewModel
-import com.getcode.navigation.flow.rememberFlowNavigator
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.AppBarWithTitle
-import com.getcode.ui.components.CircularIconButton
-import com.getcode.ui.theme.CodeScaffold
-
-@Composable
-internal fun TipCardScreen() {
- val flowNavigator = rememberFlowNavigator()
- val viewModel = flowSharedViewModel()
- val state by viewModel.stateFlow.collectAsStateWithLifecycle()
-
- CodeScaffold(
- topBar = {
- Column(
- verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x10),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- AppBarWithTitle(
- title = stringResource(R.string.title_myTipCard),
- titleAlignment = Alignment.CenterHorizontally,
- onBackIconClicked = { flowNavigator.back() },
- )
- Text(
- text = stringResource(R.string.subtitle_myTipCard),
- style = CodeTheme.typography.textLarge,
- color = CodeTheme.colors.textMain,
- )
- }
- },
- bottomBar = {
- Row(
- modifier = Modifier.fillMaxWidth()
- .navigationBarsPadding()
- .padding(bottom = CodeTheme.dimens.grid.x3),
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x5, Alignment.CenterHorizontally),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column(
- modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x3),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3),
- ) {
- CircularIconButton(
- imageSize = CodeTheme.dimens.staticGrid.x6,
- buttonSize = CodeTheme.dimens.grid.x12,
- onClick = {
- viewModel.dispatchEvent(Event.ShareTipCard)
- }
- ) { size ->
- Icon(
- painter = painterResource(R.drawable.ic_remote_send),
- contentDescription = null,
- tint = Color.White,
- // The send glyph is bottom-heavy (wide tray below a thin arrow),
- // so geometric centering leaves it sitting visually low in the
- // circle. Nudge it up slightly to optically center it.
- modifier = Modifier
- .requiredSize(size)
- .offset(y = (-1.5).dp),
- )
- }
- Text(
- text = stringResource(R.string.action_share),
- style = CodeTheme.typography.textSmall,
- color = CodeTheme.colors.textSecondary,
- )
- }
- }
- }
- ) { padding ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- state.tipCard?.let {
- CompositionLocalProvider(
- // Static display (no camera behind the card), so render it opaque at the design's
- // flattened color rather than the translucent frosted fill — the 36% alpha
- // otherwise multiplies the app's Brand background and reads incorrectly.
- // Figma flattens the card to rgb(16,16,17).
- LocalTipCardColor provides Color(0xFF101011),
- LocalTipCardBaseAlpha provides 1f,
- ) {
- ScannableRenderer(
- scannable = it,
- )
- }
- }
- }
- }
-}
diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipsScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipsScreen.kt
deleted file mode 100644
index f712e88c28..0000000000
--- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipsScreen.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-package com.flipcash.app.tipping.internal.screens
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.shape.CircleShape
-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.draw.clip
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.flipcash.app.core.AppRoute
-import com.flipcash.app.core.chat.ChatIdentifier
-import com.flipcash.app.core.tipping.TipStep
-import com.flipcash.app.tipping.internal.TipFlowViewModel
-import com.flipcash.features.tipping.R
-import com.flipcash.shared.chat.ui.ChatListRow
-import com.flipcash.shared.chat.ui.ChatRowSubtitle
-import com.flipcash.shared.chat.ui.ChatRowTrailing
-import com.flipcash.shared.chat.ui.ConversationReference
-import com.flipcash.shared.chat.ui.SubtitleText
-import com.flipcash.shared.common.ui.ContactAvatar
-import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.navigation.flow.flowSharedViewModel
-import com.getcode.theme.CodeTheme
-import com.getcode.theme.White10
-import com.getcode.ui.components.AppBarDefaults
-import com.getcode.ui.components.AppBarWithTitle
-import com.getcode.ui.theme.ButtonState
-import com.getcode.ui.theme.CodeButton
-import com.getcode.ui.theme.CodeScaffold
-
-@Composable
-internal fun TipsScreen() {
- val viewModel = flowSharedViewModel()
- val state by viewModel.stateFlow.collectAsStateWithLifecycle()
-
- val navigator = LocalCodeNavigator.current
- CodeScaffold(
- topBar = {
- AppBarWithTitle(
- title = stringResource(R.string.title_tips),
- titleAlignment = Alignment.CenterHorizontally,
- endContent = {
- AppBarDefaults.Close { navigator.hide() }
- }
- )
- }
- ) { padding ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding),
- ) {
- stickyHeader {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = CodeTheme.dimens.inset)
- .background(CodeTheme.colors.background),
- contentAlignment = Alignment.Center
- ) {
- CodeButton(
- modifier = Modifier.fillMaxWidth()
- .padding(horizontal = CodeTheme.dimens.inset),
- text = stringResource(R.string.action_showTipCard),
- buttonState = ButtonState.Filled,
- onClick = {
- navigator.navigate(TipStep.TipCard)
- },
- )
- }
- }
-
- itemsIndexed(state.tipChats) { index, chat ->
- TipChatRow(
- chat = chat,
- showDivider = index < state.tipChats.lastIndex,
- ) {
- navigator.push(AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(chat.chatId)))
- }
- }
- }
- }
-}
-
-@Composable
-private fun TipChatRow(
- chat: ConversationReference,
- modifier: Modifier = Modifier,
- showDivider: Boolean = true,
- onClick: () -> Unit,
-) {
- ChatListRow(
- modifier = modifier,
- avatar = {
- ContactAvatar(
- image = chat.image,
- displayName = chat.displayName.orEmpty(),
- modifier = Modifier
- .requiredSize(CodeTheme.dimens.staticGrid.x8)
- .clip(CircleShape),
- )
- },
- title = {
- Text(
- modifier = Modifier.weight(1f),
- text = chat.displayName.orEmpty(),
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.textMain,
- )
-
- ChatRowTrailing(
- lastActivity = chat.lastActivity,
- unreadCount = chat.unreadCount,
- canOpen = true,
- )
- },
- subtitle = {
- ChatRowSubtitle(
- isTyping = chat.isTyping,
- preview = chat.lastMessagePreview,
- fallback = {
- SubtitleText("")
- }
- )
- },
- showDivider = showDivider,
- onClick = onClick,
- )
-}
\ No newline at end of file
diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt
index 8449091644..87eaff6969 100644
--- a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt
+++ b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt
@@ -185,6 +185,7 @@ object AppBarDefaults {
fun AppBarWithTitle(
modifier: Modifier = Modifier,
title: String = "",
+ titleTextStyle: TextStyle = CodeTheme.typography.screenTitle,
titleContent: (@Composable () -> Unit)? = null,
titleAlignment: Alignment.Horizontal = Alignment.Start,
contentPadding: PaddingValues = AppBarDefaults.ContentPadding,
@@ -218,7 +219,7 @@ fun AppBarWithTitle(
}
},
titleRegion = {
- if (titleContent != null) titleContent() else AppBarDefaults.Title(text = title)
+ if (titleContent != null) titleContent() else AppBarDefaults.Title(text = title, style = titleTextStyle)
},
titleAlignment = titleAlignment,
// The caller's end actions always render; a Close (when the leading control resolves to
@@ -296,14 +297,28 @@ private fun TopAppBarBase(
)
.height(56.dp),
) { constraints ->
+ // A centered title needs a phantom leading slot (a back button's worth of width) so it
+ // stays optically centered against the end actions even when there's no real leading
+ // control. A start/end-aligned title has no such need — reserving it would only indent the
+ // title past a control that isn't there — so skip it and let the title sit flush.
+ val isCentered = titleAlignment == Alignment.CenterHorizontally
+
// Measure left icon, if provided
- val emptyLeftIconPlaceable =
+ val emptyLeftIconPlaceable = if (isCentered) {
subcompose("empty_leftIcon", emptyLeftSlot).firstOrNull()?.measure(
constraints.copy(minWidth = 0, minHeight = 0)
)
+ } else {
+ null
+ }
val leftIconPlaceable = subcompose("leftIcon", leftSlot).firstOrNull()?.measure(
constraints.copy(minWidth = 0, minHeight = 0)
)
+ // [leftSlot] wraps [leftIcon] in a 5dp-padded Box, so it has width even when the icon
+ // renders nothing. Probe the raw icon to tell an actual leading control from an empty slot.
+ val hasLeadingControl = subcompose("leftIcon_probe", leftIcon).firstOrNull()
+ ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ ?.let { it.width > 0 } ?: false
// Measure right contents, if provided
val rightContentsPlaceable =
@@ -311,9 +326,11 @@ private fun TopAppBarBase(
constraints.copy(minWidth = 0, minHeight = 0)
)
- // Calculate the remaining space for the title region
- val leftIconWidth = max(leftIconPlaceable?.width ?: 0, emptyLeftIconPlaceable?.width ?: 0)
- val isEmpty = (leftIconPlaceable?.width ?: 0) == 5.dp.roundToPx()
+ // Calculate the remaining space for the title region. A slot with no real control
+ // contributes no leading width, so a start-aligned title falls through to the flush
+ // `inset` position instead of being indented past a control that isn't there.
+ val realLeftWidth = if (hasLeadingControl) leftIconPlaceable?.width ?: 0 else 0
+ val leftIconWidth = max(realLeftWidth, emptyLeftIconPlaceable?.width ?: 0)
val rightContentsWidth = rightContentsPlaceable?.width ?: 0
val remainingWidth =
constraints.maxWidth - leftIconWidth - rightContentsWidth - (contentPadding.calculateLeftPadding(
@@ -327,13 +344,17 @@ private fun TopAppBarBase(
)
layout(constraints.maxWidth, constraints.maxHeight) {
- // Place left icon, if present
- val left = if (isEmpty) emptyLeftIconPlaceable else leftIconPlaceable
- left?.placeRelative(
- x = contentPadding.calculateLeftPadding(layoutDirection).roundToPx(),
- y = (constraints.maxHeight - (left.height)) / 2 + contentPadding.calculateTopPadding()
- .roundToPx()
- )
+ // Place the leading control only when there is one. The centered phantom slot reserves
+ // width (to keep the title optically centered) but is never drawn.
+ if (hasLeadingControl) {
+ leftIconPlaceable?.let { placeable ->
+ placeable.placeRelative(
+ x = contentPadding.calculateLeftPadding(layoutDirection).roundToPx(),
+ y = (constraints.maxHeight - placeable.height) / 2 +
+ contentPadding.calculateTopPadding().roundToPx()
+ )
+ }
+ }
// Place right contents, if present
rightContentsPlaceable?.placeRelative(
diff --git a/ui/theme/src/main/kotlin/com/getcode/theme/Type.kt b/ui/theme/src/main/kotlin/com/getcode/theme/Type.kt
index a26b4fd1ba..59bb312d73 100644
--- a/ui/theme/src/main/kotlin/com/getcode/theme/Type.kt
+++ b/ui/theme/src/main/kotlin/com/getcode/theme/Type.kt
@@ -38,6 +38,7 @@ data class CodeTypography(
val displayExtraSmall: TextStyle,
val keyboard: TextStyle,
val screenTitle: TextStyle,
+ val screenTitleLarge: TextStyle,
val textLarge: TextStyle,
val textMedium: TextStyle,
val textSmall: TextStyle,
@@ -88,6 +89,13 @@ val codeTypography = CodeTypography(
lineHeight = 24.sp,
//letterSpacing = 0.1.sp
),
+ screenTitleLarge = TextStyle(
+ fontFamily = Avenir,
+ fontSize = 33.05.sp,
+ fontWeight = FontWeight.Bold,
+ lineHeight = 24.sp,
+ //letterSpacing = 0.1.sp
+ ),
textLarge = TextStyle(
fontFamily = Avenir,
fontSize = 20.sp,