From 30f54b6b86342c3e621c830229c2d85ba603ad61 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 24 Jul 2026 11:15:32 -0400 Subject: [PATCH] chore(tip/chat): add "via Tip Card" to chat profile card for tip chats Signed-off-by: Brandon McAnsh --- .../core/src/main/res/drawable/ic_tipcode.xml | 71 +++++++++ .../core/src/main/res/values/strings.xml | 1 + .../components/ContactInfoContainer.kt | 142 +++++++++++++++--- 3 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 apps/flipcash/core/src/main/res/drawable/ic_tipcode.xml diff --git a/apps/flipcash/core/src/main/res/drawable/ic_tipcode.xml b/apps/flipcash/core/src/main/res/drawable/ic_tipcode.xml new file mode 100644 index 000000000..0e7ffc099 --- /dev/null +++ b/apps/flipcash/core/src/main/res/drawable/ic_tipcode.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 7b8e66d90..757aa4483 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -896,5 +896,6 @@ Minimum tip %1$s Tips Start at %1$s Enter a larger amount to send this tip + via Tip Card \ No newline at end of file diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt index 041e56664..89ceb320c 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.Icon @@ -27,10 +28,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.PreviewWrapper import com.flipcash.app.core.android.IntentUtils import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.messenger.internal.ChatParticipant +import com.flipcash.app.theme.FlipcashThemeWrapper import com.flipcash.features.messenger.R +import com.flipcash.services.models.UserProfile import com.getcode.theme.CodeTheme @Composable @@ -84,35 +90,55 @@ internal fun ContactInfoContainer( contract = ActivityResultContracts.StartActivityForResult() ) { onRefreshContact() } - ContactPill( + Indicator( + participant = participant, modifier = Modifier.padding(top = CodeTheme.dimens.inset), - contact = contact ) { - contact ?: return@ContactPill - val intent = IntentUtils.openContact(contact).apply { - // Remove NEW_TASK so the result callback fires when the user returns, - // not immediately. - flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() + when (participant) { + is ChatParticipant.Contact -> { + val intent = IntentUtils.openContact(participant.contact).apply { + // Remove NEW_TASK so the result callback fires when the user returns, + // not immediately. + flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() + } + launcher.launch(intent) + } + is ChatParticipant.TipUser -> Unit + null -> Unit } - launcher.launch(intent) } } } +@Composable +private fun Indicator( + participant: ChatParticipant?, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + when (participant) { + is ChatParticipant.Contact -> ContactPill( + participant.contact, + modifier = modifier, + onClick = onClick + ) + + is ChatParticipant.TipUser -> TipPill(modifier = modifier) + null -> Spacer(modifier = modifier.fillMaxWidth()) + } +} + @Composable private fun ContactPill( - contact: DeviceContact?, + contact: DeviceContact, modifier: Modifier = Modifier, onClick: () -> Unit, ) { AnimatedContent(contact) { c -> - if (c == null) { - Spacer(modifier = modifier.fillMaxWidth()) - return@AnimatedContent - } - val backgroundColor by animateColorAsState( - if (!c.isUnknown) CodeTheme.colors.surfaceVariant else CodeTheme.colors.warning.copy(alpha = 0.10f) + if (!c.isUnknown) CodeTheme.colors.surfaceVariant else CodeTheme.colors.warning.copy( + alpha = 0.10f + ) ) val contentColor by animateColorAsState( @@ -124,7 +150,10 @@ private fun ContactPill( .background(color = backgroundColor, shape = CircleShape) .clip(CircleShape) .clickable { onClick() } - .padding(horizontal = CodeTheme.dimens.grid.x2, vertical = CodeTheme.dimens.grid.x1), + .padding( + horizontal = CodeTheme.dimens.grid.x2, + vertical = CodeTheme.dimens.grid.x1 + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) ) { @@ -142,10 +171,87 @@ private fun ContactPill( ) Text( - text = if (!c.isUnknown) stringResource(R.string.label_fromYourContacts) else stringResource(R.string.label_addContact), + text = if (!c.isUnknown) stringResource(R.string.label_fromYourContacts) else stringResource( + R.string.label_addContact + ), color = contentColor, style = CodeTheme.typography.textSmall, ) } } -} \ No newline at end of file +} + +@Composable +private fun TipPill( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .background(color = CodeTheme.colors.surfaceVariant, shape = CircleShape) + .clip(CircleShape) + .padding(horizontal = CodeTheme.dimens.grid.x2, vertical = CodeTheme.dimens.grid.x1), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) + ) { + Icon( + modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4), + painter = painterResource(R.drawable.ic_tipcode), + contentDescription = null, + tint = CodeTheme.colors.textSecondary, + ) + + Text( + text = stringResource(R.string.label_viaTipCard), + color = CodeTheme.colors.textSecondary, + style = CodeTheme.typography.textSmall, + ) + } +} + +// region Previews + +@Preview +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +private fun Preview_AllStates() { + // A saved device contact: name + number + the "From your contacts" pill. + val knownContact = ChatParticipant.Contact( + DeviceContact( + e164 = "+15551234567", + androidContactId = 1L, + displayName = "Ada Lovelace", + photoUri = null, + displayNumber = "(555) 123-4567", + ) + ) + + // An unknown number: falls back to the number as the name and shows the warning + // "Add contact" pill. + val unknownContact = ChatParticipant.Contact( + DeviceContact.unknownContact( + e164 = "+15559876543", + displayNumber = "(555) 987-6543", + ) + ) + + // A tip DM's counterparty: identity from a server profile — no phone number, no pill. + val tipUser = ChatParticipant.TipUser( + userId = listOf(1.toByte()), + profile = UserProfile.Empty.copy(displayName = "Grace Hopper"), + ) + + // Fixed width so every state renders at the same size regardless of name/number length. + val cardWidth = Modifier.width(300.dp) + Column( + modifier = Modifier.padding(CodeTheme.dimens.grid.x4), + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4), + ) { + ContactInfoContainer(participant = knownContact, modifier = cardWidth) + ContactInfoContainer(participant = unknownContact, modifier = cardWidth) + ContactInfoContainer(participant = tipUser, modifier = cardWidth) + // Null participant: loading / unknown fallback avatar, name empty, no pill. + ContactInfoContainer(participant = null, modifier = cardWidth) + } +} + +// endregion \ No newline at end of file