diff --git a/apps/flipcash/app/build.gradle.kts b/apps/flipcash/app/build.gradle.kts index e2866fc80..bcdc315f7 100644 --- a/apps/flipcash/app/build.gradle.kts +++ b/apps/flipcash/app/build.gradle.kts @@ -198,6 +198,7 @@ dependencies { implementation(project(":apps:flipcash:shared:region-selection:core")) implementation(project(":apps:flipcash:shared:region-selection:ui")) implementation(project(":apps:flipcash:shared:contacts")) + implementation(project(":apps:flipcash:shared:common-ui")) implementation(project(":apps:flipcash:shared:notifications")) implementation(project(":apps:flipcash:shared:onramp:coinbase")) implementation(project(":apps:flipcash:shared:onramp:deeplinks")) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt index febe7dd96..d299271fe 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.LocalUserManager import dev.chrisbanes.haze.HazeState import com.flipcash.app.core.navigation.NavBarButton import com.flipcash.app.core.navigation.asNavBarTab @@ -24,9 +25,12 @@ import com.flipcash.app.core.navigation.destinationRoute import com.flipcash.app.core.ui.NavigationBar import com.flipcash.app.core.ui.rememberNavigationBarState import com.flipcash.app.session.LocalSessionController +import com.flipcash.services.user.AuthState +import com.flipcash.shared.common.ui.ContactAvatar import com.getcode.manager.BottomBarManager import com.getcode.navigation.core.CodeNavigator import com.getcode.theme.CodeTheme +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -72,6 +76,8 @@ internal fun AppNavigationBar( session?.state?.map { it.tipsUnreadCount } ?: flowOf(0) }.collectAsStateWithLifecycle(initialValue = 0) + val avatar = rememberProfileAvatar() + Box( modifier = Modifier .then(modifier), @@ -97,7 +103,30 @@ internal fun AppNavigationBar( navigator.replaceAll(button.destinationRoute()) }, hazeState = hazeState, + avatar = avatar, ) } } } + +/** + * The account's own photo, ready to drop into the You tab, or null when there isn't one. + * + * Gated on Ready like the rest of the profile-driven chrome: a named account restores its cached + * profile before auth completes, so an ungated read would show the previous account's avatar on + * the way in. + */ +@Composable +private fun rememberProfileAvatar(): (@Composable (Modifier) -> Unit)? { + val userManager = LocalUserManager.current + val profile by remember(userManager) { + userManager?.state + ?.filter { it.authState is AuthState.Ready } + ?.map { it.userProfile } + ?: flowOf(null) + }.collectAsStateWithLifecycle(initialValue = null) + + val picture = profile?.profilePicture ?: return null + val displayName = profile?.displayName.orEmpty() + return { modifier -> ContactAvatar(picture, displayName, modifier) } +} diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt index 2379e721a..ee86667fa 100644 --- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset @@ -87,6 +88,11 @@ fun NavigationBar( state: NavigationBarState, onButtonClick: (NavBarButton) -> Unit = {}, hazeState: HazeState? = null, + // The You tab wears the account's own photo in place of its glyph once one is set (node + // 9641:17130). This module sits below the profile layer, so the caller supplies the avatar and + // passes null when there is no photo. The modifier handed back already sizes, clips and fades + // the slot; the avatar only has to fill it. + avatar: (@Composable (Modifier) -> Unit)? = null, ) { val order = NavBarButton.tabs if (order.isEmpty()) return @@ -174,14 +180,36 @@ fun NavigationBar( contentAlignment = Alignment.Center, ) { Box { - Image( - modifier = Modifier - .size(iconSize) - .graphicsLayer { alpha = iconAlpha }, - painter = painterResource(button.icon), - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) + if (button == NavBarButton.TipCard && avatar != null) { + Box( + modifier = Modifier + .size(iconSize) + .graphicsLayer { alpha = iconAlpha } + .padding(CodeTheme.dimens.thickBorder), + ) { + avatar(Modifier.fillMaxSize().clip(CircleShape)) + // Drawn over the photo rather than behind it, so the ring survives + // whatever background the avatar paints for itself. + Box( + modifier = Modifier + .fillMaxSize() + .border( + CodeTheme.dimens.thickBorder, + Color.White, + CircleShape, + ), + ) + } + } else { + Image( + modifier = Modifier + .size(iconSize) + .graphicsLayer { alpha = iconAlpha }, + painter = painterResource(button.icon), + colorFilter = ColorFilter.tint(Color.White), + contentDescription = null, + ) + } // Overlaps the glyph's top-right corner (matching the iOS bar) rather than // floating detached above it. Full opacity regardless of tab selection — // the count must stay readable on an unselected tab.