From 9be3e9bebaa93f170bb8a43343f5f6c23d7c2513 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 7 Oct 2026 08:47:56 -0400 Subject: [PATCH 1/3] feat(profile): share a profile to chats The share-to-chats sheet now takes a subject, a group's invite or a person's profile link. Another person's profile and the You tab's "Share Profile" row both open it through a new root route, ShareProfileToChats, which carries the DM to leave out of the picker (none for your own profile). Send posts the profile URL as plain text to each picked chat, then opens the first. The You tab's share sheet follows iOS: left-aligned title, no subtitle, Share Profile and Show Profile Card rows, and a subtle Dismiss. The old profile share host, clipboard/toast plumbing and Copy Link row are gone. --- .../ui/navigation/AppScreenContent.kt | 2 + .../kotlin/com/flipcash/app/core/AppRoute.kt | 18 ++++ .../core/src/main/res/values/strings.xml | 2 +- .../flipcash/app/menu/ProfileShareScreen.kt | 30 +++---- .../menu/internal/ProfileShareViewModel.kt | 63 ++------------ .../features/messenger/build.gradle.kts | 1 + .../flipcash/app/messenger/ChatFlowScreen.kt | 5 +- .../messenger/ShareProfileToChatsScreen.kt | 73 +++++++++++++++++ .../internal/GroupInviteViewModel.kt | 46 +++++++++-- .../internal/screens/GroupInviteSheet.kt | 82 ++++++++++++++----- .../screens/profile/ChatProfileScreen.kt | 24 +++--- .../screens/profile/ChatProfileViewModel.kt | 17 ---- .../internal/screens/profile/ProfileShare.kt | 78 ------------------ .../internal/GroupInviteViewModelTest.kt | 42 +++++++++- .../profile/ChatProfileViewModelTest.kt | 2 - .../common/ui/profile/ProfileShareSheet.kt | 43 +++++----- 16 files changed, 289 insertions(+), 239 deletions(-) create mode 100644 apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ShareProfileToChatsScreen.kt delete mode 100644 apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ProfileShare.kt diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt index 0f87080e7b..2ce56b65ca 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt @@ -43,6 +43,7 @@ import com.flipcash.app.messenger.ChatPhotoViewerScreen import com.flipcash.app.messenger.E2eeLearnMoreScreen import com.flipcash.app.messenger.MuteChatScreen import com.flipcash.app.messenger.ProfileScreen +import com.flipcash.app.messenger.ShareProfileToChatsScreen import com.flipcash.app.messenger.ReportFlowScreen import com.flipcash.app.discovery.TokenDiscoveryScreen import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator @@ -143,6 +144,7 @@ fun appEntryProvider( annotatedEntry { key -> ReportFlowScreen(route = key, resultStateRegistry = resultStateRegistry) } + annotatedEntry { key -> ShareProfileToChatsScreen(key) } annotatedEntry { key -> MuteChatScreen(key.chatId, key.chatType) } annotatedEntry { key -> ChatPhotoViewerScreen(key.chatId, key.messageId) } annotatedEntry { E2eeLearnMoreScreen(forGroup = false) } diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index 4c7a7eaa1b..ace51e5743 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -466,6 +466,24 @@ sealed interface AppRoute : NavKey, Parcelable { com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet + /** + * A person's profile link, shared by the system share sheet, copied, or sent into recent + * chats. Opened from the viewer's own profile and from someone else's, so it carries what + * the link and the share card are built from. + * + * @param directChatId the viewer's DM with the person, left out of the picker so nobody is + * sent their own profile. Null for the viewer's own profile, and for a person with no DM + * yet. + */ + @Serializable + @Parcelize + data class ShareProfileToChats( + val userId: ID, + val username: String?, + val displayName: String?, + val directChatId: ChatId?, + ) : Messaging, com.getcode.navigation.Sheet + /** * What end-to-end encryption covers in a DM, opened from the DM profile's footer. A * [com.getcode.navigation.WrapContentSheet], for the reason [MuteChat] is one. diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index f9adca01a0..4bd1945967 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -1337,8 +1337,8 @@ Claim your username Not set Share User Profile + Share Profile - %1$s · @%2$s Share Profile Show Profile Card Copy Link diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/ProfileShareScreen.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/ProfileShareScreen.kt index e6a7f4ad1d..24b5ee5942 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/ProfileShareScreen.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/ProfileShareScreen.kt @@ -10,51 +10,53 @@ import com.flipcash.features.menu.R import com.flipcash.shared.common.ui.profile.ProfileShareRow import com.flipcash.shared.common.ui.profile.ProfileShareSheet import com.getcode.navigation.core.LocalCodeNavigator +import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher private const val RowShare = "share" private const val RowCard = "card" -private const val RowCopy = "copy" /** * The share sheet for the viewer's own profile, behind `AppRoute.Menu.ShareProfile`. * * A row does not act on the tap. It asks the navigator to animate the sheet away and runs the - * action once that has finished, so the system share sheet or the card screen is not opened over a - * sheet that is still on its way out. + * action once that has finished, so the card screen or the share-to-chats sheet is not opened over + * a sheet that is still on its way out. */ @Composable fun ProfileShareScreen() { val viewModel = hiltViewModel() val navigator = LocalCodeNavigator.current + val dismissSheet = LocalBottomSheetDismissDispatcher.current val profile = remember(viewModel) { viewModel.profile } - val name = profile?.displayName.orEmpty() - val handle = profile?.username - val subtitle = if (handle.isNullOrEmpty()) { - name - } else { - stringResource(R.string.subtitle_shareUserProfile, name, handle) - } val rows = listOf( ProfileShareRow(RowShare, R.drawable.ic_share_os, stringResource(R.string.action_shareProfileLink)), ProfileShareRow(RowCard, R.drawable.ic_qr_code, stringResource(R.string.action_showProfileCard)), - ProfileShareRow(RowCopy, R.drawable.ic_copy, stringResource(R.string.action_copyLink)), ) ProfileShareSheet( title = stringResource(R.string.title_shareUserProfile), - subtitle = subtitle, rows = rows, onRow = { row -> // A second tap while the sheet animates out would replace the pending action and run both. if (navigator.pendingSheetDismiss != null) return@ProfileShareSheet navigator.pendingSheetDismiss = { when (row.id) { - RowShare -> viewModel.share() + RowShare -> viewModel.currentUserId?.let { userId -> + navigator.push( + AppRoute.Messaging.ShareProfileToChats( + userId = userId, + username = profile?.username, + displayName = profile?.displayName, + // Your own profile has no DM to leave out. + directChatId = null, + ) + ) + } RowCard -> navigator.push(AppRoute.Menu.ProfileCard) - RowCopy -> viewModel.copyLink() } } }, + onDismiss = dismissSheet, ) } diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileShareViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileShareViewModel.kt index 56946d8594..9e37aa9dcc 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileShareViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/ProfileShareViewModel.kt @@ -1,78 +1,25 @@ package com.flipcash.app.menu.internal -import android.content.ClipboardManager import androidx.lifecycle.ViewModel -import com.flipcash.app.bills.share.TipCodePreviewCache -import com.flipcash.app.core.extensions.setText -import com.flipcash.app.core.tipping.TipCardOwner -import com.flipcash.app.core.toast.SystemToastController -import com.flipcash.app.core.util.Linkify -import com.flipcash.app.shareable.ShareSheetController -import com.flipcash.app.shareable.Shareable -import com.flipcash.features.menu.R import com.flipcash.services.models.UserProfile import com.flipcash.services.user.UserManager import com.flipcash.shared.tipping.TippingCoordinator -import com.getcode.util.resources.ResourceHelper +import com.getcode.opencode.model.core.ID import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import javax.inject.Inject -/** - * What the share sheet's rows do for the viewer's own profile. The sheet only chooses; the - * navigation host runs one of these once the sheet has finished leaving. - */ +/** What the You tab's share sheet needs to address the viewer's own profile. */ @HiltViewModel internal class ProfileShareViewModel @Inject constructor( private val userManager: UserManager, private val tippingCoordinator: TippingCoordinator, - private val tipCodePreviewCache: TipCodePreviewCache, - private val shareable: ShareSheetController, - private val clipboardManager: ClipboardManager, - private val toastController: SystemToastController, - private val resources: ResourceHelper, ) : ViewModel() { - // Not viewModelScope: the actions run after the sheet has finished leaving, and this view model - // is cleared with the sheet's entry before then. Each action is short and holds nothing but - // singletons, so there is nothing to leak by letting it finish. - private val actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - /** The profile being shared, read once when the sheet opens. */ val profile: UserProfile? get() = userManager.state.value.userProfile - /** The link a profile is shared by. Null only when there is no signed-in user to address. */ - private val link: String? - get() = tippingCoordinator.currentUserId?.let { userId -> - Linkify.tipcard(TipCardOwner.preferringUsername(profile?.username, userId)) - } - - /** Hands the profile link, with its card preview if ready, to the system share sheet. */ - fun share() { - val userId = tippingCoordinator.currentUserId ?: return - // Attach the eagerly-rendered preview if it's ready; null shares the URL alone. - actionScope.launch { - shareable.present( - Shareable.Profile( - userId = userId, - displayName = profile?.displayName, - username = profile?.username, - preview = tipCodePreviewCache.get(userId), - ) - ) - } - } - - fun copyLink() { - val link = link ?: return - clipboardManager.setText( - text = link, - label = resources.getString(R.string.title_clipboardLabelTipCardLink), - ) - toastController.showToast(R.string.action_copied, replacePrevious = true) - } + /** Null only when there is no signed-in user to address. */ + val currentUserId: ID? + get() = tippingCoordinator.currentUserId } diff --git a/apps/flipcash/features/messenger/build.gradle.kts b/apps/flipcash/features/messenger/build.gradle.kts index 4ebd4e40b9..756ccc8980 100644 --- a/apps/flipcash/features/messenger/build.gradle.kts +++ b/apps/flipcash/features/messenger/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { testImplementation(testFixtures(project(":apps:flipcash:shared:analytics"))) testImplementation(testFixtures(project(":libs:coroutines"))) implementation(project(":apps:flipcash:shared:blob")) + implementation(project(":apps:flipcash:shared:bills")) implementation(project(":apps:flipcash:shared:blocklist")) implementation(project(":apps:flipcash:shared:chat")) implementation(project(":apps:flipcash:shared:chat-ui")) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt index 19666e9e89..550aa4ad65 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt @@ -25,6 +25,7 @@ import com.flipcash.app.messenger.internal.ChatSubject import com.flipcash.app.messenger.internal.ChatViewModel import com.flipcash.app.messenger.internal.GroupInviteViewModel import com.flipcash.app.messenger.internal.screens.GroupInviteSheet +import com.flipcash.app.messenger.internal.screens.ShareToChatsSubject import com.flipcash.app.messenger.internal.screens.MessengerScreen import com.flipcash.app.messenger.internal.screens.EmojiPickerViewModel import com.flipcash.app.messenger.internal.screens.ReactionPickerSheet @@ -232,7 +233,7 @@ private fun FlowGroupInviteSheet() { val dismissSheet = LocalBottomSheetDismissDispatcher.current LaunchedEffect(inviteViewModel, state.chatId) { - state.chatId?.let(inviteViewModel::inviteTo) + state.chatId?.let(inviteViewModel::exclude) } // Returned to whichever screen opened the sheet (see openGroupInvite), which navigates. The @@ -248,7 +249,7 @@ private fun FlowGroupInviteSheet() { GroupInviteSheet( inviteUrl = state.groupInviteUrl, - group = state.subject as? ChatSubject.Group, + subject = ShareToChatsSubject.Group(state.subject as? ChatSubject.Group), state = inviteState, onShare = { viewModel.dispatchEvent(ChatViewModel.Event.InviteLinkShared) }, onCopy = { viewModel.dispatchEvent(ChatViewModel.Event.CopyInviteLink) }, diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ShareProfileToChatsScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ShareProfileToChatsScreen.kt new file mode 100644 index 0000000000..52bd51fdb9 --- /dev/null +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ShareProfileToChatsScreen.kt @@ -0,0 +1,73 @@ +package com.flipcash.app.messenger + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.chat.ChatIdentifier +import com.flipcash.app.core.tipping.TipCardOwner +import com.flipcash.app.core.util.Linkify +import com.flipcash.app.messenger.internal.GroupInviteViewModel +import com.flipcash.app.messenger.internal.screens.GroupInviteSheet +import com.flipcash.app.messenger.internal.screens.ShareToChatsSubject +import com.getcode.navigation.core.LocalCodeNavigator +import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher +import com.getcode.ui.utils.rememberKeyboardController + +/** + * A person's profile link, shared by the system share sheet, copied, or sent into recent chats, + * behind `AppRoute.Messaging.ShareProfileToChats`. + * + * Opened from the root navigator by the viewer's own profile and by someone else's, so it names the + * chat to leave out on the route: the DM with that person, or nothing for the viewer's own. + */ +@Composable +fun ShareProfileToChatsScreen(route: AppRoute.Messaging.ShareProfileToChats) { + val viewModel = hiltViewModel() + val state by viewModel.state.collectAsStateWithLifecycle() + val navigator = LocalCodeNavigator.current + // Exit through the sheet so it animates down rather than having its scene deleted mid-frame. + val dismissSheet = LocalBottomSheetDismissDispatcher.current + val keyboard = rememberKeyboardController() + + val link = remember(route.userId, route.username) { + Linkify.tipcard(TipCardOwner.preferringUsername(route.username, route.userId)) + } + val preview = remember(viewModel, route.userId) { viewModel.previewFor(route.userId) } + + LaunchedEffect(viewModel, route.directChatId) { viewModel.exclude(route.directChatId) } + + // The chat is opened once the sheet is gone, so it is not pushed under one still on its way + // out. The keyboard goes first: the message field has it up, and leaving with it still open + // drags the screen behind out from under it. + LaunchedEffect(viewModel) { + viewModel.invited.collect { chatId -> + keyboard.hideIfVisible { + navigator.pendingSheetDismiss = { + navigator.push(AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(chatId))) + } + } + } + } + + GroupInviteSheet( + inviteUrl = link, + subject = ShareToChatsSubject.User( + userId = route.userId, + username = route.username, + displayName = route.displayName, + preview = preview, + ), + state = state, + // A person's link has no group analytics to fire. + onShare = {}, + onCopy = { viewModel.copyProfileLink(link) }, + onToggle = viewModel::toggle, + onMessageChanged = viewModel::onMessageChanged, + onInvite = { viewModel.invite(link) }, + onDismiss = dismissSheet, + ) +} diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModel.kt index 9355064c9e..750d2c2719 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModel.kt @@ -1,13 +1,19 @@ package com.flipcash.app.messenger.internal +import android.content.ClipboardManager import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.flipcash.app.bills.share.TipCodePreviewCache +import com.flipcash.app.core.extensions.setText +import com.flipcash.app.core.share.TipCodePreview +import com.flipcash.features.messenger.R import com.flipcash.services.models.chat.ChatId import com.flipcash.services.user.UserManager import com.flipcash.shared.chat.ChatCoordinator import com.flipcash.shared.chat.chatListFeed import com.flipcash.shared.chat.ui.ConversationReference import com.flipcash.shared.chat.ui.toConversationReference +import com.getcode.opencode.model.core.ID import com.getcode.util.resources.ResourceHelper import com.getcode.utils.trace import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,7 +34,8 @@ import javax.inject.Inject /** * The invite sheet's "Recent Chats" (nodes 10330:19387, 10330:19549, 10329:12104): which chats, - * 1:1 or group, are picked, the message to go with the link, and sending both to each. + * 1:1 or group, are picked, the message to go with the link, and sending both to each. It serves a + * group's invite and a person's profile link alike; they differ only in which chat is left out. * * Scoped to the sheet rather than kept on the conversation's view model: a selection means nothing * once the sheet is gone, and the send has to outlive nothing but the sheet, which waits for it. @@ -38,13 +45,18 @@ internal class GroupInviteViewModel @Inject constructor( private val chatCoordinator: ChatCoordinator, private val userManager: UserManager, private val resources: ResourceHelper, + private val clipboardManager: ClipboardManager, + private val tipCodePreviewCache: TipCodePreviewCache, ) : ViewModel() { data class State( /** Null until the feed first emits, so an empty list means there are no chats to offer. */ val recentChats: List? = null, - /** The group the invites are for, which is left out of its own list. */ - val groupId: ChatId? = null, + /** + * The chat that is left out of the list: the group being invited to, or the DM with the + * person being shared. Null for the viewer's own profile, which leaves nothing out. + */ + val excludedChatId: ChatId? = null, /** Picked chats in the order they were tapped. The first is where the send lands. */ val selection: List = emptyList(), val message: String = "", @@ -53,9 +65,12 @@ internal class GroupInviteViewModel @Inject constructor( /** The message bar only comes up once there is someone to send to (node 10330:19549). */ val showsComposer: Boolean get() = selection.isNotEmpty() - /** [recentChats] without the group being invited to: sending a group its own link is a no-op. */ + /** + * [recentChats] without [excludedChatId]: sending a group its own link is a no-op, and + * sending a person their own profile is pointless. + */ val invitable: List? - get() = recentChats?.filterNot { it.chatId == groupId } + get() = recentChats?.filterNot { it.chatId == excludedChatId } } private val _state = MutableStateFlow(State()) @@ -81,14 +96,27 @@ internal class GroupInviteViewModel @Inject constructor( } } - /** Names the group the sheet invites to, so it drops out of the list and cannot be picked. */ - fun inviteTo(groupId: ChatId) { - _state.update { it.copy(groupId = groupId, selection = it.selection - groupId) } + /** Names the chat the sheet leaves out, so it drops out of the list and cannot be picked. */ + fun exclude(chatId: ChatId?) { + _state.update { state -> + state.copy(excludedChatId = chatId, selection = state.selection.filterNot { it == chatId }) + } + } + + /** The rendered share-sheet card for [userId], if one is ready. Null shares the link alone. */ + fun previewFor(userId: ID): TipCodePreview? = tipCodePreviewCache.get(userId) + + /** Puts a person's profile [link] on the clipboard. */ + fun copyProfileLink(link: String) { + clipboardManager.setText( + text = link, + label = resources.getString(R.string.title_clipboardLabelTipCardLink), + ) } fun toggle(chatId: ChatId) { _state.update { state -> - if (state.sending || chatId == state.groupId) return@update state + if (state.sending || chatId == state.excludedChatId) return@update state val selection = if (chatId in state.selection) { state.selection - chatId } else { diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt index 31fcbb8aea..c75f861de7 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt @@ -65,6 +65,7 @@ import com.flipcash.app.core.chat.ChatStep import com.flipcash.app.core.chat.GroupInviteResult import com.flipcash.app.core.media.rememberMediaUrl import com.flipcash.app.core.share.SharePreviewImage +import com.flipcash.app.core.share.TipCodePreview import com.flipcash.app.messenger.internal.ChatSubject import com.flipcash.app.messenger.internal.GroupInviteViewModel import com.flipcash.app.shareable.LocalShareController @@ -74,6 +75,7 @@ import com.flipcash.services.models.chat.BlobAccessContext import com.flipcash.services.models.chat.ChatId import com.flipcash.shared.chat.ui.ConversationReference import com.flipcash.shared.common.ui.ContactAvatar +import com.getcode.opencode.model.core.ID import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.results.NavResultOrCanceled import com.getcode.navigation.results.navigateForResult @@ -112,25 +114,47 @@ internal fun CodeNavigator.openGroupInvite() { } /** - * Node 10329:12104 — handing out the link to a group, and sending it straight into the + * What [GroupInviteSheet] hands out: a group's invite link, or a person's profile link. The two + * differ in their copy, in what the Share tile gives the system share sheet, and in whether the + * group's analytics fire; the rest of the sheet is the same. + */ +internal sealed interface ShareToChatsSubject { + /** + * @param group what is being invited to, for the group's name and picture. The sheet only opens + * on a group, so null is the frame before the subject resolves. + */ + data class Group(val group: ChatSubject.Group?) : ShareToChatsSubject + + /** + * @param preview the tip-code image for the system share sheet's card, when one is rendered. + */ + data class User( + val userId: ID, + val username: String?, + val displayName: String?, + val preview: TipCodePreview?, + ) : ShareToChatsSubject +} + +/** + * Node 10329:12104 — handing out a link to a group or a person, and sending it straight into the * viewer's other chats. * - * The Share and Copy tiles carry the same URL, built once by - * [com.flipcash.app.core.util.Linkify.groupChatInvite] from the chat's id: there is no invite RPC, - * the id *is* the invite, and one builder is what keeps the shared link, the copied link and the - * sent link identical. + * The Share and Copy tiles carry the same URL, built once by the caller: for a group by + * [com.flipcash.app.core.util.Linkify.groupChatInvite] from the chat's id (there is no invite RPC, + * the id *is* the invite), for a person by [com.flipcash.app.core.util.Linkify.tipcard]. One + * builder is what keeps the shared link, the copied link and the sent link identical. * * @param inviteUrl null for anyone who has nobody to invite — a DM, or a group this viewer has not * joined — in which case the sheet is the title bar alone. Both entry points are gated on the same * value, so this is a race with a leave rather than a state to design for. - * @param group what is being invited to, for the group's name and picture. The sheet only opens on - * a group, so null is the frame before the subject resolves. + * @param subject what is being handed out. * @param state the Recent Chats selection and the message to send with the link. */ @Composable internal fun GroupInviteSheet( inviteUrl: String?, - group: ChatSubject.Group?, + subject: ShareToChatsSubject, state: GroupInviteViewModel.State, onShare: () -> Unit, onCopy: () -> Unit, @@ -145,6 +169,7 @@ internal fun GroupInviteSheet( // Resolved here rather than in the share controller: the stored download URL expires, and // re-minting it needs the chat's profile to authorize it — neither of which the shareable // module can see. Same size and access as the avatar the conversation bar draws. + val group = (subject as? ShareToChatsSubject.Group)?.group val picture = group?.picture val pictureUrl = rememberMediaUrl( media = picture, @@ -214,13 +239,21 @@ internal fun GroupInviteSheet( onShare() scope.launch { shareController.present( - Shareable.GroupInvite( - url = inviteUrl, - title = group?.groupTitle, - imageUrl = pictureUrl.url, - imageCacheKey = picture - ?.cacheKeyForSize(SharePreviewImage.TARGET_PX), - ) + when (subject) { + is ShareToChatsSubject.Group -> Shareable.GroupInvite( + url = inviteUrl, + title = group?.groupTitle, + imageUrl = pictureUrl.url, + imageCacheKey = picture + ?.cacheKeyForSize(SharePreviewImage.TARGET_PX), + ) + is ShareToChatsSubject.User -> Shareable.Profile( + userId = subject.userId, + displayName = subject.displayName, + username = subject.username, + preview = subject.preview, + ) + } ) } }, @@ -229,8 +262,11 @@ internal fun GroupInviteSheet( modifier = Modifier.weight(1f), icon = if (copied) R.drawable.ic_check else R.drawable.ic_copy, label = stringResource( - if (copied) R.string.action_inviteLinkCopied - else R.string.action_copyInviteLink + when { + copied -> R.string.action_inviteLinkCopied + subject is ShareToChatsSubject.User -> R.string.action_copyLink + else -> R.string.action_copyInviteLink + } ), onClick = { onCopy() @@ -292,7 +328,9 @@ internal fun GroupInviteSheet( ) } .onSizeChanged { headerHeight = with(density) { it.height.toDp() } }, - title = stringResource(R.string.title_invitePeople), + title = stringResource( + if (subject is ShareToChatsSubject.User) R.string.title_shareProfileToChats else R.string.title_invitePeople + ), titleAlignment = Alignment.CenterHorizontally, contentPadding = PaddingValues(HeaderPadding), endContent = { AppBarDefaults.Close(hazeState = hazeState, onClick = onDismiss) }, @@ -318,6 +356,9 @@ internal fun GroupInviteSheet( .onSizeChanged { fieldHeight = with(density) { it.height.toDp() } }, hazeState = hazeState, sending = state.sending, + sendLabel = stringResource( + if (subject is ShareToChatsSubject.User) R.string.action_send else R.string.action_invite + ), onMessageChanged = onMessageChanged, onInvite = onInvite, ) @@ -440,13 +481,14 @@ private fun RecentChatRow( * and the sheet's view model reads it at send time. * * The chat screen's own composer and glass, so typing here feels like typing in a chat, but - * without its outline, since the Figma pill has none. An Invite label replaces the send arrow + * without its outline, since the Figma pill has none. A text label replaces the send arrow * because the link goes out whether or not a message is typed. */ @Composable private fun InviteComposer( hazeState: HazeState, sending: Boolean, + sendLabel: String, onMessageChanged: (String) -> Unit, onInvite: () -> Unit, modifier: Modifier = Modifier, @@ -471,7 +513,7 @@ private fun InviteComposer( hint = stringResource(R.string.hint_addAMessage), state = message, submit = ChatInputSubmit.Action( - label = stringResource(R.string.action_invite), + label = sendLabel, busy = sending, perform = onInvite, ), diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileScreen.kt index d6a3de5d45..d5f5ba0408 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileScreen.kt @@ -117,8 +117,6 @@ internal fun PersonProfileScreen( val person = state.participant as? ChatParticipant.TipUser val isSelf = person != null && person.userId == state.selfId val pinned = if (isSelf) null else state.pinnedAction - val share = rememberProfileShare() - var shareOpen by remember { mutableStateOf(false) } var menuOpen by remember { mutableStateOf(false) } var pinnedHeight by remember { mutableStateOf(0.dp) } @@ -184,7 +182,18 @@ internal fun PersonProfileScreen( ProfileActionButton( icon = ImageVector.vectorResource(R.drawable.ic_share_os), contentDescription = stringResource(R.string.action_share), - onClick = { shareOpen = true }, + onClick = { + // The DM is what "Open Chat" opens, so it is the one chat + // that has nothing to gain from being sent this person. + navigator.push( + AppRoute.Messaging.ShareProfileToChats( + userId = person.userId, + username = person.profile.username, + displayName = person.profile.displayName, + directChatId = state.dmChatId, + ) + ) + }, ) } }, @@ -263,15 +272,6 @@ internal fun PersonProfileScreen( } } } - - if (shareOpen && person != null) { - ProfileShareSheetHost( - person = person, - onShare = { share(person) }, - onCopyLink = viewModel::copyLink, - onDismiss = { shareOpen = false }, - ) - } } @Composable diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModel.kt index e737ea6be3..f7a9d0d154 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModel.kt @@ -1,15 +1,10 @@ package com.flipcash.app.messenger.internal.screens.profile -import android.content.ClipboardManager import androidx.lifecycle.viewModelScope import com.flipcash.app.blocklist.BlocklistCoordinator import com.flipcash.app.contacts.ContactCoordinator import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatParticipant -import com.flipcash.app.core.extensions.setText -import com.flipcash.app.core.tipping.TipCardOwner -import com.flipcash.app.core.toast.SystemToastController -import com.flipcash.app.core.util.Linkify import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.messenger.internal.payment.StartChattingPayer import com.flipcash.features.messenger.R @@ -68,8 +63,6 @@ internal class ChatProfileViewModel @Inject constructor( private val tipPaymentDelegate: TipPaymentDelegate, private val e2eePolicy: E2eePolicy, private val startChattingPayer: StartChattingPayer, - private val clipboardManager: ClipboardManager, - private val toastController: SystemToastController, ) : BaseViewModel( initialState = State(selfId = userManager.accountId), updateStateForEvent = updateStateForEvent, @@ -196,16 +189,6 @@ internal class ChatProfileViewModel @Inject constructor( data class OpenScreen(val route: AppRoute, val asSheet: Boolean = false) : Event } - /** Puts the person's profile link on the clipboard, the one Share hands out. */ - fun copyLink() { - val person = stateFlow.value.participant as? ChatParticipant.TipUser ?: return - clipboardManager.setText( - text = Linkify.tipcard(TipCardOwner.preferringUsername(person.profile.username, person.userId)), - label = resources.getString(R.string.title_clipboardLabelTipCardLink), - ) - toastController.showToast(R.string.action_copied, replacePrevious = true) - } - init { eventFlow .filterIsInstance() diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ProfileShare.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ProfileShare.kt deleted file mode 100644 index 077db911b6..0000000000 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/ProfileShare.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.flipcash.app.messenger.internal.screens.profile - -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.res.stringResource -import com.flipcash.app.core.chat.ChatParticipant -import com.flipcash.app.shareable.LocalShareController -import com.flipcash.app.shareable.Shareable -import com.flipcash.features.messenger.R -import com.flipcash.shared.common.ui.profile.ProfileShareRow -import com.flipcash.shared.common.ui.profile.ProfileShareSheet -import com.getcode.theme.CodeTheme -import kotlinx.coroutines.launch - -/** Hands a person's profile to the system share sheet. */ -@Composable -internal fun rememberProfileShare(): (ChatParticipant.TipUser) -> Unit { - val shareController = LocalShareController.current - val scope = rememberCoroutineScope() - return remember(shareController, scope) { - { user -> - scope.launch { - shareController.present( - Shareable.Profile( - userId = user.userId, - displayName = user.profile.displayName, - username = user.profile.username, - ) - ) - } - } - } -} - -private const val RowShare = "share" -private const val RowCopy = "copy" - -/** - * Share Profile and Copy Link for someone else's profile. No profile-card row: the card is the - * viewer's own to show. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun ProfileShareSheetHost( - person: ChatParticipant.TipUser, - onShare: () -> Unit, - onCopyLink: () -> Unit, - onDismiss: () -> Unit, -) { - val handle = person.profile.username - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - containerColor = CodeTheme.colors.background, - ) { - LightStatusBarIcons() - ProfileShareSheet( - title = stringResource(R.string.title_shareUserProfile), - subtitle = if (handle.isNullOrEmpty()) { - person.profile.displayName - } else { - stringResource(R.string.subtitle_shareUserProfile, person.profile.displayName, handle) - }, - rows = listOf( - ProfileShareRow(RowShare, R.drawable.ic_share_os, stringResource(R.string.action_shareProfileLink)), - ProfileShareRow(RowCopy, R.drawable.ic_copy, stringResource(R.string.action_copyLink)), - ), - onRow = { row -> - onDismiss() - if (row.id == RowShare) onShare() else onCopyLink() - }, - ) - } -} diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModelTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModelTest.kt index a90113796d..bbf868f67f 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModelTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/GroupInviteViewModelTest.kt @@ -63,7 +63,13 @@ class GroupInviteViewModelTest { Dispatchers.resetMain() } - private fun viewModel() = GroupInviteViewModel(chatCoordinator, userManager, resources) + private fun viewModel() = GroupInviteViewModel( + chatCoordinator, + userManager, + resources, + clipboardManager = mockk(relaxed = true), + tipCodePreviewCache = mockk(relaxed = true), + ) @Test fun `the message bar is hidden until a chat is picked`() { @@ -173,13 +179,45 @@ class GroupInviteViewModelTest { val model = viewModel() advanceUntilIdle() - model.inviteTo(group) + model.exclude(group) model.toggle(group) assertEquals(listOf(alice), model.state.value.invitable?.map { it.chatId }) assertEquals(emptyList(), model.state.value.selection) } + @Test + fun `a person's DM is left out of the chats their profile is sent to`() = runTest(scheduler) { + val dm = ChatId(ByteArray(32) { 7 }) + every { chatCoordinator.feed(*anyVararg()) } returns flowOf( + listOf(summary(alice, ChatType.TIP_DM), summary(dm, ChatType.TIP_DM)), + ) + val model = viewModel() + advanceUntilIdle() + + model.exclude(dm) + model.toggle(dm) + + assertEquals(listOf(alice), model.state.value.invitable?.map { it.chatId }) + assertEquals(emptyList(), model.state.value.selection) + } + + @Test + fun `no exclusion offers every chat`() = runTest(scheduler) { + val group = ChatId(ByteArray(32) { 9 }) + every { chatCoordinator.feed(*anyVararg()) } returns flowOf( + listOf(summary(alice, ChatType.TIP_DM), summary(group, ChatType.GROUP)), + ) + val model = viewModel() + advanceUntilIdle() + + model.exclude(null) + model.toggle(alice) + + assertEquals(listOf(alice, group), model.state.value.invitable?.map { it.chatId }) + assertEquals(listOf(alice), model.state.value.selection) + } + @Test fun `nothing is sent with nobody picked`() = runTest(scheduler) { viewModel().invite(INVITE_URL) diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModelTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModelTest.kt index 0e65b9517e..4f2bdbd0e1 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModelTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/screens/profile/ChatProfileViewModelTest.kt @@ -88,8 +88,6 @@ class ChatProfileViewModelTest { tipPaymentDelegate = tipPaymentDelegate, e2eePolicy = mockk(relaxed = true), startChattingPayer = payer, - clipboardManager = mockk(relaxed = true), - toastController = mockk(relaxed = true), ) private fun openFull(model: ChatProfileViewModel) = diff --git a/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt b/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt index 234ad66682..5bb71da4c6 100644 --- a/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt +++ b/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt @@ -6,11 +6,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper import com.flipcash.app.theme.FlipcashThemeWrapper @@ -18,6 +17,8 @@ import com.flipcash.shared.common.ui.R import com.getcode.theme.CodeTheme import com.getcode.ui.components.AppBarWithTitle import com.getcode.ui.components.ChoiceRow +import com.getcode.ui.theme.ButtonState +import com.getcode.ui.theme.CodeButton /** * One choice in a [ProfileShareSheet]. The caller decides which rows exist, so a person's profile @@ -31,18 +32,19 @@ data class ProfileShareRow( ) /** - * The content of a profile's share sheet: [title] over [subtitle] (who is being shared), then one - * row per entry in [rows]. + * The content of the You tab's share sheet: [title] left-aligned, one row per entry in [rows], and + * a subtle Dismiss. * * It only draws and reports the tap. Hosting it in a sheet, dismissing that sheet, and running the - * chosen action once the sheet has finished leaving are the caller's. + * chosen action once the sheet has finished leaving are the caller's. Other people's profiles and + * groups share through the share-to-chats sheet instead. */ @Composable fun ProfileShareSheet( title: String, - subtitle: String, rows: List, onRow: (ProfileShareRow) -> Unit, + onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -50,33 +52,27 @@ fun ProfileShareSheet( .fillMaxWidth() .navigationBarsPadding(), ) { - AppBarWithTitle( - title = title, - titleAlignment = Alignment.CenterHorizontally, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - text = subtitle, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) + AppBarWithTitle(title = title) Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = CodeTheme.dimens.inset) - .padding(vertical = CodeTheme.dimens.grid.x4), + .padding(bottom = CodeTheme.dimens.grid.x4), verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), ) { rows.forEach { row -> ChoiceRow( label = row.label, - icon = row.icon, + icon = painterResource(row.icon), onClick = { onRow(row) }, ) } + CodeButton( + modifier = Modifier.fillMaxWidth(), + buttonState = ButtonState.Subtle, + text = stringResource(R.string.action_dismiss), + onClick = onDismiss, + ) } } } @@ -87,12 +83,11 @@ fun ProfileShareSheet( private fun Preview_ProfileShareSheet() { ProfileShareSheet( title = "Share User Profile", - subtitle = "Ada Lovelace · @ada", rows = listOf( ProfileShareRow("share", R.drawable.ic_share_os, "Share Profile"), ProfileShareRow("card", R.drawable.ic_file_download, "Show Profile Card"), - ProfileShareRow("copy", R.drawable.ic_copy, "Copy Link"), ), onRow = {}, + onDismiss = {}, ) } From 39c1c4e6ce7254d57f9c043a3dd2af20118ff5b8 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 7 Oct 2026 08:54:34 -0400 Subject: [PATCH 2/3] feat(profile): present the share-to-chats sheet full screen iOS presents ShareToChatsSheet as a cover for both a group invite and a profile share. Mark ChatStep.InviteToGroup and AppRoute.Messaging.ShareProfileToChats as FullscreenSheet so they rest at the full screen height with square top corners. --- .../src/main/kotlin/com/flipcash/app/core/AppRoute.kt | 4 +++- .../main/kotlin/com/flipcash/app/core/chat/ChatStep.kt | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index ace51e5743..211d3e9586 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -474,6 +474,8 @@ sealed interface AppRoute : NavKey, Parcelable { * @param directChatId the viewer's DM with the person, left out of the picker so nobody is * sent their own profile. Null for the viewer's own profile, and for a person with no DM * yet. + * + * Full screen, as iOS presents it as a cover. */ @Serializable @Parcelize @@ -482,7 +484,7 @@ sealed interface AppRoute : NavKey, Parcelable { val username: String?, val displayName: String?, val directChatId: ChatId?, - ) : Messaging, com.getcode.navigation.Sheet + ) : Messaging, com.getcode.navigation.Sheet, com.getcode.navigation.FullscreenSheet /** * What end-to-end encryption covers in a DM, opened from the DM profile's footer. A diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt index 1f308f9006..0c505a1266 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt @@ -55,11 +55,16 @@ sealed interface ChatStep : FlowStep, Parcelable { * * Not a [WrapContentSheet]: the chat list can run past the screen, and a wrap-content body is * measured at its full height and then clipped, taking the message bar under it off the bottom. - * A full sheet bounds the body, so the list scrolls and the bar stays on screen. + * A full sheet bounds the body, so the list scrolls and the bar stays on screen. Full screen, + * as on iOS, which presents it as a cover. */ @Parcelize @Serializable - data object InviteToGroup : ChatStep, NavigationRetVal, Sheet + data object InviteToGroup : + ChatStep, + NavigationRetVal, + Sheet, + com.getcode.navigation.FullscreenSheet @Parcelize @Serializable From 6cc8904a67124c8354b14b4d06c82894aee24c16 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 7 Oct 2026 09:53:47 -0400 Subject: [PATCH 3/3] fix(profile): draw the share sheet on the Add Money sheet colour --- .../com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt b/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt index 5bb71da4c6..e5477e72b0 100644 --- a/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt +++ b/apps/flipcash/shared/common-ui/src/main/kotlin/com/flipcash/shared/common/ui/profile/ProfileShareSheet.kt @@ -1,6 +1,7 @@ package com.flipcash.shared.common.ui.profile import androidx.annotation.DrawableRes +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -50,6 +51,8 @@ fun ProfileShareSheet( Column( modifier = modifier .fillMaxWidth() + // The Add Money sheet's colour, as iOS draws this sheet. + .background(CodeTheme.colors.bannerThemed) .navigationBarsPadding(), ) { AppBarWithTitle(title = title)