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,