From b9f7fe52e3f071fe3386c314cc3186ae92bedcc4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 22:56:29 -0400 Subject: [PATCH] refactor(profile): make UserProfile.displayName non-nullable Aligns the model with the proto (display name is always present as an empty string, never absent). Updates construction sites to use UserProfile.Empty / "" and drops now-redundant null handling at non-null receivers. --- .../app/directsend/internal/ContactListBuilder.kt | 2 +- .../flipcash/app/messenger/internal/ChatParticipant.kt | 2 +- .../com/flipcash/app/messenger/internal/ChatViewModel.kt | 2 +- .../app/myaccount/internal/UserProfileScreenContent.kt | 4 ++-- .../app/myaccount/internal/UserProfileViewModel.kt | 6 +++--- .../internal/ContactMethodsViewModelStateTest.kt | 6 +++--- .../myaccount/internal/UserProfileScreenContentTest.kt | 6 ------ .../flipcash/app/tipping/internal/TipFlowViewModel.kt | 2 +- .../shared/chat/internal/delegates/FeedSyncDelegate.kt | 2 +- .../persistence/sources/mapper/chat/ChatEntityMapper.kt | 9 ++------- .../com/flipcash/shared/profile/ProfileCoordinator.kt | 2 +- .../flipcash/services/controllers/ProfileController.kt | 4 +--- .../kotlin/com/flipcash/services/models/UserProfile.kt | 4 ++-- 13 files changed, 19 insertions(+), 32 deletions(-) diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt index 116815955..d9a5693d7 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt @@ -84,7 +84,7 @@ internal class ContactListBuilder @Inject constructor( .firstOrNull { !isSelf(it) } ?: return@mapNotNull null val phone = otherMember.userProfile.verifiedPhoneNumber val formattedPhone = phone?.let { phoneUtils.formatNumber(it) } - val displayName = otherMember.userProfile.displayName?.takeIf { it.isNotBlank() } + val displayName = otherMember.userProfile.displayName.takeIf { it.isNotBlank() } ?: formattedPhone ?: return@mapNotNull null diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatParticipant.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatParticipant.kt index 4f831f324..c2159f74d 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatParticipant.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatParticipant.kt @@ -23,6 +23,6 @@ internal sealed interface ChatParticipant { } data class TipUser(val userId: ID, val profile: UserProfile) : ChatParticipant { - override val displayName: String get() = profile.displayName.orEmpty() + override val displayName: String get() = profile.displayName } } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index 34ee35124..134ce4951 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -422,7 +422,7 @@ internal class ChatViewModel @Inject constructor( val other = members.firstOrNull { it.userId != selfId } if (other != null) { val profile = other.userProfile - val hasIdentity = !profile.displayName.isNullOrBlank() || + val hasIdentity = profile.displayName.isNotBlank() || !profile.verifiedPhoneNumber.isNullOrBlank() !hasIdentity } else false diff --git a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContent.kt b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContent.kt index f18ad2bfa..95a4123b0 100644 --- a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContent.kt +++ b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContent.kt @@ -264,7 +264,7 @@ internal fun UserProfileScreenContent( @Composable private fun ProfileHeader( - displayName: String?, + displayName: String, profilePicture: MediaItem?, onEditName: () -> Unit, onEditPhoto: () -> Unit, @@ -290,7 +290,7 @@ private fun ProfileHeader( horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1), ) { Text( - text = displayName?.takeIf { it.isNotEmpty() } + text = displayName.takeIf { it.isNotEmpty() } ?: stringResource(R.string.subtitle_noDisplayName), style = CodeTheme.typography.textLarge, color = CodeTheme.colors.textMain, diff --git a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileViewModel.kt b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileViewModel.kt index 2087782ec..560300f9c 100644 --- a/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileViewModel.kt +++ b/apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/UserProfileViewModel.kt @@ -45,7 +45,7 @@ internal class UserProfileViewModel @Inject constructor( defaultDispatcher = dispatchers.Default, ) { internal data class State( - val displayName: String? = null, + val displayName: String = "", val profilePicture: MediaItem? = null, val phone: VerifiableContactMethod? = null, val email: VerifiableContactMethod? = null, @@ -58,7 +58,7 @@ internal class UserProfileViewModel @Inject constructor( internal sealed interface Event { data class OnProfileUpdated( - val displayName: String?, + val displayName: String, val profilePicture: MediaItem?, val phone: VerifiableContactMethod?, val email: VerifiableContactMethod?, @@ -102,7 +102,7 @@ internal class UserProfileViewModel @Inject constructor( dispatchEvent( Event.OnProfileUpdated( - displayName = profile?.displayName, + displayName = profile?.displayName.orEmpty(), profilePicture = profile?.profilePicture, // Carry the contact (value + verified) so unverified entries still show. phone = profile?.phoneNumber, diff --git a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/ContactMethodsViewModelStateTest.kt b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/ContactMethodsViewModelStateTest.kt index 9624eabb6..1ebb43085 100644 --- a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/ContactMethodsViewModelStateTest.kt +++ b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/ContactMethodsViewModelStateTest.kt @@ -15,7 +15,7 @@ class ContactMethodsViewModelStateTest { @Test fun `default state has null profile fields`() { val state = UserProfileViewModel.State() - assertNull(state.displayName) + assertTrue(state.displayName.isEmpty()) assertNull(state.phone) assertNull(state.email) assertFalse(state.phoneLinkedForPayment) @@ -58,7 +58,7 @@ class ContactMethodsViewModelStateTest { fun `OnProfileUpdated with null values`() { val updated = reduce( UserProfileViewModel.Event.OnProfileUpdated( - displayName = null, + displayName = "", profilePicture = null, phone = null, email = null, @@ -66,7 +66,7 @@ class ContactMethodsViewModelStateTest { socialAccounts = emptyList(), ) )(UserProfileViewModel.State()) - assertNull(updated.displayName) + assertTrue(updated.displayName.isEmpty()) assertNull(updated.phone) assertNull(updated.email) assertFalse(updated.phoneLinkedForPayment) diff --git a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContentTest.kt b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContentTest.kt index e833334f9..f50d6ffca 100644 --- a/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContentTest.kt +++ b/apps/flipcash/features/myaccount/src/test/kotlin/com/flipcash/app/myaccount/internal/UserProfileScreenContentTest.kt @@ -41,12 +41,6 @@ class UserProfileScreenContentTest { composeTestRule.onNodeWithText("Alice").assertIsDisplayed() } - @Test - fun `no display name placeholder when null`() { - setScreen(UserProfileViewModel.State(displayName = null)) - composeTestRule.onNodeWithText("No display name set").assertIsDisplayed() - } - @Test fun `no display name placeholder when empty`() { setScreen(UserProfileViewModel.State(displayName = "")) diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt index 3fe06a7d3..07c48273c 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt @@ -64,7 +64,7 @@ internal class TipFlowViewModel @Inject constructor( stateFlow.map { it.resumed }.distinctUntilChanged(), ) { profile, resumed -> buildList { - val hasProfile = profile?.displayName != null // TODO: explicitly not required right now && profile.profilePicture != null + val hasProfile = !profile?.displayName.isNullOrEmpty() // TODO: explicitly not required right now && profile.profilePicture != null when { // Post-setup handoff: land on TipCard alone, with a close affordance. No Tips // underneath — closing exits, and reopening from home lands on the Tips list. diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/FeedSyncDelegate.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/FeedSyncDelegate.kt index f5d6604e7..54b248d33 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/FeedSyncDelegate.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/FeedSyncDelegate.kt @@ -91,7 +91,7 @@ class FeedSyncDelegate @Inject constructor( // identified by user id and have no phone by design, so they are never dropped. if (chatType == ChatType.CONTACT_DM) { val profile = otherMember.userProfile - val hasIdentity = !profile.displayName.isNullOrBlank() || + val hasIdentity = profile.displayName.isNotBlank() || !profile.verifiedPhoneNumber.isNullOrBlank() if (!hasIdentity) return@mapNotNull null } diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt index 6bb0f3a01..966773645 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt @@ -150,12 +150,7 @@ class ChatEntityMapper @Inject constructor() { fun toMember(entity: ChatMemberEntity): ChatMember { return ChatMember( userId = entity.userIdHex.hexToId(), - userProfile = entity.userProfileJson?.toDomain() ?: UserProfile( - displayName = null, - socialAccounts = emptyList(), - phoneNumber = null, - email = null, - ), + userProfile = entity.userProfileJson?.toDomain() ?: UserProfile.Empty, pointers = entity.pointersJson?.map { it.toDomain() } ?: emptyList(), ) } @@ -284,7 +279,7 @@ private fun UserProfile.toSerialized(): UserProfileSerialized = UserProfileSeria ) private fun UserProfileSerialized.toDomain(): UserProfile = UserProfile( - displayName = displayName, + displayName = displayName.orEmpty(), socialAccounts = socialAccounts.map { it.toDomain() }, phoneNumber = phoneNumber, email = email, diff --git a/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt b/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt index 4457fbfb3..b94c2fb45 100644 --- a/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt +++ b/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt @@ -82,7 +82,7 @@ private data class CachedProfile( val email: VerifiableContactMethod? = null, ) { fun toDomain(): UserProfile = UserProfile( - displayName = displayName, + displayName = displayName.orEmpty(), socialAccounts = socialAccounts.mapNotNull { it.toDomain() }, phoneNumber = phoneNumber, email = email, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt index 051d6763b..bebb7cbfa 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt @@ -51,9 +51,7 @@ class ProfileController @Inject constructor( throw error } - UserProfile( - displayName = null, - socialAccounts = emptyList(), + UserProfile.Empty.copy( phoneNumber = unverifiedPhone, email = unverifiedEmail, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt index 18dedcc08..d8d9d320c 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt @@ -3,7 +3,7 @@ package com.flipcash.services.models import com.flipcash.services.models.chat.MediaItem data class UserProfile( - val displayName: String?, + val displayName: String, val socialAccounts: List, val phoneNumber: VerifiableContactMethod?, val email: VerifiableContactMethod?, @@ -19,7 +19,7 @@ data class UserProfile( companion object { val Empty = UserProfile( - displayName = null, + displayName = "", socialAccounts = emptyList(), phoneNumber = null, email = null,