From 76da88eacc4f3760dfae737f3eac46199f050a91 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 30 Aug 2026 02:12:13 +0200 Subject: [PATCH 1/8] fix: polish payment request ui --- .../bitkit/ui/components/PubkyContactRow.kt | 6 +- .../CreatePaymentRequestScreen.kt | 398 ++++++++++-------- .../paymentrequests/PaymentRequestsScreen.kt | 38 +- changelog.d/next/payment-request-ui.fixed.md | 1 + 4 files changed, 244 insertions(+), 199 deletions(-) create mode 100644 changelog.d/next/payment-request-ui.fixed.md diff --git a/app/src/main/java/to/bitkit/ui/components/PubkyContactRow.kt b/app/src/main/java/to/bitkit/ui/components/PubkyContactRow.kt index f2e837add0..1ae15563a2 100644 --- a/app/src/main/java/to/bitkit/ui/components/PubkyContactRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/PubkyContactRow.kt @@ -32,7 +32,7 @@ fun PubkyContactRow( modifier: Modifier = Modifier, isSelected: Boolean? = null, isEnabled: Boolean = true, - verticalPadding: Dp = 12.dp, + verticalPadding: Dp = 24.dp, selectionColor: Color = Colors.PubkyGreen, ) { Row( @@ -59,13 +59,13 @@ fun PubkyContactRow( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(1f) ) { - BodyS( + Text13Up( text = profile.truncatedPublicKey, color = Colors.White64, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - BodySSB( + BodyMSB( text = profile.name, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 37f30aea50..3ad855a5be 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -6,6 +6,7 @@ package to.bitkit.ui.screens.paymentrequests import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -34,7 +35,6 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -58,11 +58,13 @@ import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.FillWidth import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.PubkyContactRow import to.bitkit.ui.components.TextInput +import to.bitkit.ui.components.UnitButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -141,110 +143,152 @@ internal fun PaymentRequestDetailsContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() - .padding(horizontal = 16.dp) .testTag("PaymentRequestDetails") ) { SheetTopBar( - titleText = stringResource(R.string.wallet__payment_request), + titleText = stringResource( + if (isEditingAmount) { + R.string.wallet__payment_request_amount + } else { + R.string.wallet__payment_request + } + ), onBack = onBack, ) - Caption13Up(text = stringResource(R.string.wallet__payment_request_amount), color = Colors.White64) - VerticalSpacer(8.dp) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth(), - ) { - NumberPadTextField( - viewModel = amountInputViewModel, - onClick = { isEditingAmount = true }, - modifier = Modifier - .weight(1f) - .testTag("PaymentRequestAmountField"), - ) - IconButton( - onClick = { isEditingAmount = true }, - modifier = Modifier - .size(48.dp) - .testTag("PaymentRequestEditAmount"), - ) { - Icon( - painter = painterResource(R.drawable.ic_pencil_simple), - contentDescription = stringResource(R.string.common__edit), - tint = Colors.White, - modifier = Modifier.size(24.dp), - ) - } - } - if (isEditingAmount) { - FillHeight() - NumberPad( - viewModel = amountInputViewModel, - availableHeight = 210.dp, - modifier = Modifier.testTag("PaymentRequestNumberPad"), - ) - VerticalSpacer(12.dp) - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = { isEditingAmount = false }, - modifier = Modifier.testTag("PaymentRequestAmountDone"), - ) - } else { - VerticalSpacer(20.dp) - Caption13Up(text = stringResource(R.string.wallet__payment_request_note), color = Colors.White64) - VerticalSpacer(8.dp) - TextInput( - value = note, - onValueChange = { note = it.take(256) }, - placeholder = stringResource(R.string.wallet__payment_request_note_placeholder), - maxLines = 2, - modifier = Modifier - .fillMaxWidth() - .testTag("PaymentRequestNote"), - ) - VerticalSpacer(20.dp) - Caption13Up(text = stringResource(R.string.wallet__payment_request_expires), color = Colors.White64) - VerticalSpacer(8.dp) - Row(modifier = Modifier.fillMaxWidth()) { - PaymentRequestExpiration.entries.forEach { option -> - val isSelected = option == expiration - Column( - horizontalAlignment = Alignment.CenterHorizontally, + BoxWithConstraints { + val maxHeight = this.maxHeight + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + VerticalSpacer(16.dp) + if (isEditingAmount) { + NumberPadTextField( + viewModel = amountInputViewModel, modifier = Modifier - .weight(1f) - .clickableAlpha { expiration = option } - .semantics { - role = Role.RadioButton - selected = isSelected - } - .testTag("PaymentRequestExpiry${option.name}"), + .fillMaxWidth() + .testTag("PaymentRequestAmountField"), + ) + FillHeight(min = 12.dp) + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth(), ) { - BodyS(text = option.title(), color = if (isSelected) Colors.White else Colors.White64) - VerticalSpacer(8.dp) - HorizontalDivider( - thickness = 2.dp, - color = if (isSelected) Colors.White else Colors.White16, + FillWidth() + UnitButton( + onClick = { amountInputViewModel.switchUnit(currencies) }, + modifier = Modifier.testTag("PaymentRequestNumberPadUnit"), ) } - } - } - FillHeight() - PrimaryButton( - text = stringResource(R.string.wallet__payment_request_choose_recipient), - enabled = amountState.sats > 0, - onClick = { - onContinue( - PaykitPaymentRequestDraft( - amountSats = amountState.sats.toULong(), - note = note.trim(), - expiresAt = Clock.System.now() + expiration.duration, + VerticalSpacer(16.dp) + HorizontalDivider() + NumberPad( + viewModel = amountInputViewModel, + currencies = currencies, + availableHeight = maxHeight, + modifier = Modifier.testTag("PaymentRequestNumberPad"), + ) + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = { isEditingAmount = false }, + modifier = Modifier.testTag("PaymentRequestAmountDone"), + ) + } else { + Caption13Up( + text = stringResource(R.string.wallet__payment_request_amount), + color = Colors.White64, + ) + VerticalSpacer(8.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + NumberPadTextField( + viewModel = amountInputViewModel, + onClick = { isEditingAmount = true }, + modifier = Modifier + .weight(1f) + .testTag("PaymentRequestAmountField"), ) + IconButton( + onClick = { isEditingAmount = true }, + modifier = Modifier + .size(48.dp) + .testTag("PaymentRequestEditAmount"), + ) { + Icon( + painter = painterResource(R.drawable.ic_pencil_simple), + contentDescription = stringResource(R.string.common__edit), + tint = Colors.White, + modifier = Modifier.size(24.dp), + ) + } + } + VerticalSpacer(16.dp) + Caption13Up( + text = stringResource(R.string.wallet__payment_request_note), + color = Colors.White64, ) - }, - modifier = Modifier.testTag("PaymentRequestAmountContinue"), - ) + VerticalSpacer(16.dp) + TextInput( + value = note, + onValueChange = { note = it.take(256) }, + placeholder = stringResource(R.string.wallet__payment_request_note_placeholder), + minLines = 4, + maxLines = 4, + modifier = Modifier + .fillMaxWidth() + .testTag("PaymentRequestNote"), + ) + VerticalSpacer(16.dp) + Caption13Up( + text = stringResource(R.string.wallet__payment_request_expires), + color = Colors.White64, + ) + VerticalSpacer(8.dp) + Row(modifier = Modifier.fillMaxWidth()) { + PaymentRequestExpiration.entries.forEach { option -> + val isSelected = option == expiration + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .weight(1f) + .clickableAlpha { expiration = option } + .semantics { + role = Role.RadioButton + selected = isSelected + } + .testTag("PaymentRequestExpiry${option.name}"), + ) { + BodyS( + text = option.title(), + color = if (isSelected) Colors.White else Colors.White64, + ) + VerticalSpacer(8.dp) + HorizontalDivider( + thickness = 2.dp, + color = if (isSelected) Colors.White else Colors.White16, + ) + } + } + } + FillHeight() + PrimaryButton( + text = stringResource(R.string.wallet__payment_request_choose_recipient), + enabled = amountState.sats > 0, + onClick = { + onContinue( + PaykitPaymentRequestDraft( + amountSats = amountState.sats.toULong(), + note = note.trim(), + expiresAt = Clock.System.now() + expiration.duration, + ) + ) + }, + modifier = Modifier.testTag("PaymentRequestAmountContinue"), + ) + } + VerticalSpacer(16.dp) + } } - VerticalSpacer(16.dp) } } @@ -304,7 +348,6 @@ internal fun PaymentRequestRecipientContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() - .padding(horizontal = 16.dp) .testTag("PaymentRequestRecipient") ) { SheetTopBar( @@ -324,42 +367,48 @@ internal fun PaymentRequestRecipientContent( } }, ) - Caption13Up(text = stringResource(R.string.wallet__payment_request_recipient), color = Colors.White64) - VerticalSpacer(8.dp) - TextInput( - value = query, - onValueChange = { query = it }, - placeholder = stringResource(R.string.wallet__payment_request_enter_pubky), - singleLine = true, - textStyle = AppTextStyles.BodyM, - trailingIcon = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .clickableAlpha { - query = PubkyPublicKeyFormat.bounded(onPaste()) - } - .padding(horizontal = 12.dp) - .testTag("PaymentRequestRecipientPaste"), - ) { - Icon( - painter = painterResource(R.drawable.ic_clipboard_text), - contentDescription = null, - tint = Colors.White, - modifier = Modifier.size(24.dp), - ) - BodyMSB(text = stringResource(R.string.wallet__payment_request_paste)) - } - }, - modifier = Modifier - .fillMaxWidth() - .testTag("PaymentRequestRecipientSearch"), + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + VerticalSpacer(16.dp) + Caption13Up(text = stringResource(R.string.wallet__payment_request_recipient), color = Colors.White64) + VerticalSpacer(8.dp) + TextInput( + value = query, + onValueChange = { query = it }, + placeholder = stringResource(R.string.wallet__payment_request_enter_pubky), + singleLine = true, + textStyle = AppTextStyles.BodyM, + trailingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .clickableAlpha { + query = PubkyPublicKeyFormat.bounded(onPaste()) + } + .padding(horizontal = 12.dp) + .testTag("PaymentRequestRecipientPaste"), + ) { + Icon( + painter = painterResource(R.drawable.ic_clipboard_text), + contentDescription = null, + tint = Colors.White, + modifier = Modifier.size(24.dp), + ) + BodyMSB(text = stringResource(R.string.wallet__payment_request_paste)) + } + }, + modifier = Modifier + .fillMaxWidth() + .testTag("PaymentRequestRecipientSearch"), + ) + VerticalSpacer(16.dp) + } + Caption13Up( + text = stringResource(R.string.contacts__contacts_header), + color = Colors.White64, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), ) - VerticalSpacer(24.dp) - Caption13Up(text = stringResource(R.string.contacts__contacts_header), color = Colors.White64) - VerticalSpacer(8.dp) - HorizontalDivider(color = Colors.White10) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) LazyColumn(modifier = Modifier.weight(1f)) { if (recipients.isEmpty()) { item { @@ -372,10 +421,9 @@ internal fun PaymentRequestRecipientContent( } ), color = Colors.White64, - textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(top = 24.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) .testTag("PaymentRequestRecipientUnavailable"), ) } @@ -389,11 +437,11 @@ internal fun PaymentRequestRecipientContent( onClick = { selectedTarget = target }, isSelected = target == selectedTarget, isEnabled = !isCreating, - verticalPadding = 16.dp, - selectionColor = Colors.Brand, - modifier = Modifier.testTag("PaymentRequestContact${contact.publicKey}"), + modifier = Modifier + .padding(horizontal = 16.dp) + .testTag("PaymentRequestContact${contact.publicKey}"), ) - HorizontalDivider(color = Colors.White10) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) } } PrimaryButton( @@ -404,7 +452,9 @@ internal fun PaymentRequestRecipientContent( val target = selectedTarget ?: return@PrimaryButton onSend(target) }, - modifier = Modifier.testTag("PaymentRequestSend"), + modifier = Modifier + .padding(horizontal = 16.dp) + .testTag("PaymentRequestSend"), ) VerticalSpacer(16.dp) } @@ -429,52 +479,52 @@ internal fun PaymentRequestSentContent( onDone: () -> Unit, ) { Column( - horizontalAlignment = Alignment.Start, modifier = modifier .fillMaxSize() .gradientBackground() .navigationBarsPadding() - .padding(horizontal = 16.dp) .testTag("PaymentRequestSent"), ) { SheetTopBar(titleText = stringResource(R.string.wallet__payment_request_sent_title)) - VerticalSpacer(32.dp) - Image( - painter = painterResource(R.drawable.check), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(256.dp) - .testTag("PaymentRequestSentCheck"), - ) - VerticalSpacer(32.dp) - Display( - text = stringResource(R.string.wallet__payment_request_sent_headline) - .withAccent(accentColor = Colors.Purple), - ) - VerticalSpacer(12.dp) - BodyM( - text = stringResource(R.string.wallet__payment_request_sent_description), - color = Colors.White64, - textAlign = TextAlign.Start, - modifier = Modifier.fillMaxWidth(), - ) - VerticalSpacer(24.dp) - PaymentRequestCard( - request = request, - contact = contact, - compactSubtitle = if (request.deliveryStatus == PaykitPaymentRequestDeliveryStatus.Sent) { - stringResource(R.string.wallet__payment_request_waiting) - } else { - stringResource(R.string.wallet__payment_request_sending) - }, - ) - VerticalSpacer(32.dp) - PrimaryButton( - text = stringResource(R.string.common__ok), - onClick = onDone, - ) VerticalSpacer(16.dp) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + ) { + Display( + text = stringResource(R.string.wallet__payment_request_sent_headline) + .withAccent(accentColor = Colors.Purple), + ) + VerticalSpacer(8.dp) + BodyM( + text = stringResource(R.string.wallet__payment_request_sent_description), + color = Colors.White64, + ) + Image( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag("PaymentRequestSentCheck"), + ) + PaymentRequestCard( + request = request, + contact = contact, + compactSubtitle = if (request.deliveryStatus == PaykitPaymentRequestDeliveryStatus.Sent) { + stringResource(R.string.wallet__payment_request_waiting) + } else { + stringResource(R.string.wallet__payment_request_sending) + }, + ) + VerticalSpacer(16.dp) + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onDone, + ) + VerticalSpacer(16.dp) + } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt index abdab879f5..08f16ad723 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt @@ -280,7 +280,8 @@ internal fun PaymentRequestsContent( PaymentRequestCard( request = request, contact = contacts.contactFor(request), - compactSubtitle = paymentRequestDate(request), + compactSubtitle = request.note?.takeIf { it.isNotBlank() } + ?: paymentRequestDate(request), ) } } @@ -355,7 +356,6 @@ private fun ActivePaymentRequestCard( PaymentRequestCard( request = request, contact = contact, - compactSubtitle = paymentRequestDateTime(request), isRejecting = isRejecting, onPay = { onPay(request.id) }, onReject = { onReject(request) }, @@ -408,16 +408,6 @@ private fun paymentRequestDate(request: PaykitPaymentRequest): String = request. uiDateText(it.epochSeconds.toULong(), UiDateStyle.DATE) } ?: paymentRequestStatus(request) -@Composable -private fun paymentRequestDateTime(request: PaykitPaymentRequest): String = request.createdAt?.let { - val timestamp = it.epochSeconds.toULong() - stringResource( - R.string.wallet__payment_request_timestamp, - uiDateText(timestamp, UiDateStyle.DATE), - uiDateText(timestamp, UiDateStyle.TIME), - ) -} ?: paymentRequestStatus(request) - @Composable private fun paymentRequestStatus(request: PaykitPaymentRequest): String { if (request.lifecycleState == PaymentRequestLifecycleState.PROPOSED && request.isExpired(Clock.System.now())) { @@ -463,13 +453,17 @@ internal fun PaymentRequestCard( onReject: (() -> Unit)? = null, ) { val displayContact = contact ?: PubkyProfile.placeholder(request.counterparty) - val subtitle = compactSubtitle ?: request.createdAt?.let { - val timestamp = it.epochSeconds.toULong() - val date = uiDateText(timestamp, UiDateStyle.DATE) - val time = uiDateText(timestamp, UiDateStyle.TIME) - val formattedTimestamp = stringResource(R.string.wallet__payment_request_timestamp, date, time) - stringResource(R.string.wallet__payment_request_contact_timestamp, displayContact.name, formattedTimestamp) - } ?: displayContact.name + val subtitle = compactSubtitle + ?: request.note?.takeIf { it.isNotBlank() } + ?: request.createdAt?.let { + val timestamp = it.epochSeconds.toULong() + stringResource( + R.string.wallet__payment_request_timestamp, + uiDateText(timestamp, UiDateStyle.DATE), + uiDateText(timestamp, UiDateStyle.TIME), + ) + } + ?: displayContact.name Card( colors = CardDefaults.cardColors(containerColor = Colors.Gray6), @@ -494,13 +488,13 @@ internal fun PaymentRequestCard( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(16.dp), ) { - PubkyContactAvatar(profile = displayContact, size = 40.dp) + PubkyContactAvatar(profile = displayContact) Column(modifier = Modifier.weight(1f)) { BodyMSB( - text = request.note ?: stringResource(R.string.wallet__payment_request), + text = displayContact.name, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/changelog.d/next/payment-request-ui.fixed.md b/changelog.d/next/payment-request-ui.fixed.md new file mode 100644 index 0000000000..a6cf160a52 --- /dev/null +++ b/changelog.d/next/payment-request-ui.fixed.md @@ -0,0 +1 @@ +Payment request screens now follow the same spacing, contact rows, amount keypad, and success layout as the rest of Bitkit. \ No newline at end of file From 70f45518ebde89fa5ff8a487e7e620069b437063 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 30 Aug 2026 02:13:11 +0200 Subject: [PATCH 2/8] chore: rename changelog fragment --- changelog.d/next/{payment-request-ui.fixed.md => 1196.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{payment-request-ui.fixed.md => 1196.fixed.md} (100%) diff --git a/changelog.d/next/payment-request-ui.fixed.md b/changelog.d/next/1196.fixed.md similarity index 100% rename from changelog.d/next/payment-request-ui.fixed.md rename to changelog.d/next/1196.fixed.md From 2dcd654ba4453d02af1b9d37830649f95bae1460 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 30 Aug 2026 13:53:17 +0200 Subject: [PATCH 3/8] fix: drop trailing commas after modifiers --- .../CreatePaymentRequestScreen.kt | 28 +++++++++---------- .../paymentrequests/PaymentRequestsScreen.kt | 16 +++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 3ad855a5be..9d8c2e21d5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -169,12 +169,12 @@ internal fun PaymentRequestDetailsContent( FillHeight(min = 12.dp) Row( verticalAlignment = Alignment.Bottom, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) { FillWidth() UnitButton( onClick = { amountInputViewModel.switchUnit(currencies) }, - modifier = Modifier.testTag("PaymentRequestNumberPadUnit"), + modifier = Modifier.testTag("PaymentRequestNumberPadUnit") ) } VerticalSpacer(16.dp) @@ -183,12 +183,12 @@ internal fun PaymentRequestDetailsContent( viewModel = amountInputViewModel, currencies = currencies, availableHeight = maxHeight, - modifier = Modifier.testTag("PaymentRequestNumberPad"), + modifier = Modifier.testTag("PaymentRequestNumberPad") ) PrimaryButton( text = stringResource(R.string.common__continue), onClick = { isEditingAmount = false }, - modifier = Modifier.testTag("PaymentRequestAmountDone"), + modifier = Modifier.testTag("PaymentRequestAmountDone") ) } else { Caption13Up( @@ -199,7 +199,7 @@ internal fun PaymentRequestDetailsContent( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) { NumberPadTextField( viewModel = amountInputViewModel, @@ -218,7 +218,7 @@ internal fun PaymentRequestDetailsContent( painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = stringResource(R.string.common__edit), tint = Colors.White, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp) ) } } @@ -283,7 +283,7 @@ internal fun PaymentRequestDetailsContent( ) ) }, - modifier = Modifier.testTag("PaymentRequestAmountContinue"), + modifier = Modifier.testTag("PaymentRequestAmountContinue") ) } VerticalSpacer(16.dp) @@ -356,13 +356,13 @@ internal fun PaymentRequestRecipientContent( IconButton( onClick = onEditExpiration, enabled = !isCreating, - modifier = Modifier.testTag("PaymentRequestEditExpiration"), + modifier = Modifier.testTag("PaymentRequestEditExpiration") ) { Icon( painter = painterResource(R.drawable.ic_timer), contentDescription = stringResource(R.string.wallet__payment_request_edit_expiration), tint = Colors.White, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp) ) } }, @@ -392,7 +392,7 @@ internal fun PaymentRequestRecipientContent( painter = painterResource(R.drawable.ic_clipboard_text), contentDescription = null, tint = Colors.White, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp) ) BodyMSB(text = stringResource(R.string.wallet__payment_request_paste)) } @@ -406,7 +406,7 @@ internal fun PaymentRequestRecipientContent( Caption13Up( text = stringResource(R.string.contacts__contacts_header), color = Colors.White64, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) ) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) LazyColumn(modifier = Modifier.weight(1f)) { @@ -571,7 +571,7 @@ private fun PaymentRequestDetailsPreview() { initialDraft = previewDraft, onBack = {}, onContinue = {}, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } @@ -589,7 +589,7 @@ private fun PaymentRequestRecipientPreview() { onEditExpiration = {}, onPaste = { "" }, onSend = {}, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } @@ -604,7 +604,7 @@ private fun PaymentRequestSentPreview() { request = previewCreatedRequest, contact = PubkyProfile.placeholder(previewTarget.publicKey), onDone = {}, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt index 08f16ad723..4a7404bed5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt @@ -129,12 +129,12 @@ internal fun PaymentRequestsSheetContent( BodyM( text = stringResource(R.string.wallet__payment_requests_review), color = Colors.White64, - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier.align(Alignment.CenterHorizontally) ) VerticalSpacer(24.dp) LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { items(requests.take(3), key = { it.lazyListKey }) { request -> PaymentRequestCard( @@ -150,7 +150,7 @@ internal fun PaymentRequestsSheetContent( SecondaryButton( text = stringResource(R.string.wallet__payment_requests_not_now), onClick = onNotNow, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) PrimaryButton( text = stringResource(R.string.wallet__payment_requests_see_all), @@ -489,7 +489,7 @@ internal fun PaymentRequestCard( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(16.dp) ) { PubkyContactAvatar(profile = displayContact) Column(modifier = Modifier.weight(1f)) { @@ -529,11 +529,11 @@ internal fun PaymentRequestCard( Icon( painter = painterResource(R.drawable.ic_x), contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(16.dp) ) }, size = ButtonSize.Small, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) PrimaryButton( text = stringResource(R.string.wallet__payment_request_pay), @@ -543,11 +543,11 @@ internal fun PaymentRequestCard( Icon( painter = painterResource(R.drawable.ic_coins), contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(16.dp) ) }, size = ButtonSize.Small, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) } } From 1c47766b5c7b2f5c1ccb7644996e1d3557de225e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 1 Sep 2026 01:10:34 +0200 Subject: [PATCH 4/8] fix: match payment request sheet chrome --- .../CreatePaymentRequestScreenTest.kt | 22 ++- .../PaymentRequestsScreenTest.kt | 5 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 1 + .../java/to/bitkit/ui/components/Money.kt | 19 ++- .../CreatePaymentRequestScreen.kt | 159 ++++++++++++------ .../paymentrequests/PaymentRequestsScreen.kt | 95 +++++------ .../screens/wallets/receive/ReceiveSheet.kt | 42 ++++- app/src/main/res/values/strings.xml | 5 +- 8 files changed, 226 insertions(+), 122 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt index aa46daadeb..39d98a5636 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt @@ -53,6 +53,25 @@ class CreatePaymentRequestScreenTest { composeTestRule.onNodeWithTag("PaymentRequestNote").assertDoesNotExist() } + @Test + fun detailsWithRecipientShowsCardAndSendRequest() { + composeTestRule.setContent { + AppThemeSurface { + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = draft, + onBack = {}, + onContinue = {}, + recipient = PubkyProfile.placeholder(target.publicKey), + ) + } + } + + composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsDisplayed() + composeTestRule.onNodeWithText("Send Request").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").assertDoesNotExist() + } + @Test fun recipientShowsEligibleContactAndSendAction() { composeTestRule.setContent { @@ -61,6 +80,7 @@ class CreatePaymentRequestScreenTest { targets = persistentListOf(target), contacts = persistentListOf(PubkyProfile.placeholder(target.publicKey)), isCreating = false, + onBack = {}, onEditExpiration = {}, onPaste = { target.publicKey }, onSend = {}, @@ -95,7 +115,7 @@ class CreatePaymentRequestScreenTest { composeTestRule.onNodeWithTag("PaymentRequestSent").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestSentCheck").assertIsDisplayed() composeTestRule.onNodeWithText("PAYMENT REQUESTED").assertIsDisplayed() - composeTestRule.onNodeWithText("Waiting for payment").assertIsDisplayed() + composeTestRule.onNodeWithText("Dinner").assertIsDisplayed() } private val draft = PaykitPaymentRequestDraft( diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt index 35fd856b3d..d2b45eab55 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt @@ -107,8 +107,9 @@ class PaymentRequestsScreenTest { composeTestRule.onNodeWithTag("PaymentRequestRowaccepted").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestRowoutgoing").assertIsDisplayed() - composeTestRule.onNodeWithText("Waiting for", substring = true).assertIsDisplayed() - composeTestRule.onNodeWithText("PAYMENT REQUESTS").assertIsDisplayed() + composeTestRule.onNodeWithText("Dinner").assertIsDisplayed() + composeTestRule.onNodeWithText("INCOMING").assertDoesNotExist() + composeTestRule.onNodeWithText("OUTGOING").assertIsDisplayed() composeTestRule.onNodeWithText("TODAY").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestCreate").assertIsDisplayed() } diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index b34954e346..8c79801fef 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -746,6 +746,7 @@ private fun RootNavHost( appViewModel = appViewModel, onBack = { navController.popBackStack() }, onRequestPayment = { + navController.popBackStack() appViewModel.showSheet(Sheet.Receive(route = ReceiveRoute.PaymentRequestDetails)) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 467404a6e2..5aea606c4d 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -45,25 +45,34 @@ fun MoneyDisplay( fun MoneyCell( sats: Long, modifier: Modifier = Modifier, + secondaryText: String? = null, ) { val currencies = LocalCurrencies.current Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(2.dp), - modifier = modifier, + modifier = modifier ) { rememberMoneyText(sats = sats, unit = currencies.primaryDisplay, showSymbol = true)?.let { text -> BodyMSB( text = text.withAccent(accentColor = Colors.White64), - modifier = Modifier.testTag("MoneyPrimary"), + modifier = Modifier.testTag("MoneyPrimary") ) } - rememberMoneyText(sats = sats, unit = currencies.primaryDisplay.not(), showSymbol = true)?.let { text -> + if (secondaryText != null) { CaptionB( - text = text.withAccent(accentColor = Colors.White64), + text = secondaryText, color = Colors.White64, - modifier = Modifier.testTag("MoneySecondary"), + modifier = Modifier.testTag("MoneySecondary") ) + } else { + rememberMoneyText(sats = sats, unit = currencies.primaryDisplay.not(), showSymbol = true)?.let { text -> + CaptionB( + text = text.withAccent(accentColor = Colors.White64), + color = Colors.White64, + modifier = Modifier.testTag("MoneySecondary") + ) + } } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 9d8c2e21d5..9737e19891 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -106,12 +106,16 @@ fun PaymentRequestDetailsScreen( initialDraft: PaykitPaymentRequestDraft, onBack: () -> Unit, onContinue: (PaykitPaymentRequestDraft) -> Unit, + recipient: PubkyProfile? = null, + isCreating: Boolean = false, ) { PaymentRequestDetailsContent( amountInputViewModel = amountInputViewModel, initialDraft = initialDraft, onBack = onBack, onContinue = onContinue, + recipient = recipient, + isCreating = isCreating, ) } @@ -122,6 +126,8 @@ internal fun PaymentRequestDetailsContent( initialDraft: PaykitPaymentRequestDraft, onBack: () -> Unit, onContinue: (PaykitPaymentRequestDraft) -> Unit, + recipient: PubkyProfile? = null, + isCreating: Boolean = false, ) { val currencies = LocalCurrencies.current val amountState by amountInputViewModel.uiState.collectAsStateWithLifecycle() @@ -137,6 +143,7 @@ internal fun PaymentRequestDetailsContent( currencies, ) } + BackHandler(enabled = isCreating) {} Column( modifier = modifier @@ -164,7 +171,7 @@ internal fun PaymentRequestDetailsContent( viewModel = amountInputViewModel, modifier = Modifier .fillMaxWidth() - .testTag("PaymentRequestAmountField"), + .testTag("PaymentRequestAmountField") ) FillHeight(min = 12.dp) Row( @@ -187,6 +194,7 @@ internal fun PaymentRequestDetailsContent( ) PrimaryButton( text = stringResource(R.string.common__continue), + enabled = amountState.sats > 0, onClick = { isEditingAmount = false }, modifier = Modifier.testTag("PaymentRequestAmountDone") ) @@ -197,7 +205,7 @@ internal fun PaymentRequestDetailsContent( ) VerticalSpacer(8.dp) Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ) { @@ -206,38 +214,56 @@ internal fun PaymentRequestDetailsContent( onClick = { isEditingAmount = true }, modifier = Modifier .weight(1f) - .testTag("PaymentRequestAmountField"), + .testTag("PaymentRequestAmountField") ) - IconButton( - onClick = { isEditingAmount = true }, + Icon( + painter = painterResource(R.drawable.ic_pencil_simple), + contentDescription = stringResource(R.string.common__edit), + tint = Colors.White, modifier = Modifier - .size(48.dp) - .testTag("PaymentRequestEditAmount"), - ) { - Icon( - painter = painterResource(R.drawable.ic_pencil_simple), - contentDescription = stringResource(R.string.common__edit), - tint = Colors.White, - modifier = Modifier.size(24.dp) - ) - } + .size(24.dp) + .clickableAlpha { isEditingAmount = true } + .testTag("PaymentRequestEditAmount") + ) } VerticalSpacer(16.dp) Caption13Up( text = stringResource(R.string.wallet__payment_request_note), color = Colors.White64, ) - VerticalSpacer(16.dp) + VerticalSpacer(8.dp) TextInput( value = note, onValueChange = { note = it.take(256) }, placeholder = stringResource(R.string.wallet__payment_request_note_placeholder), - minLines = 4, - maxLines = 4, + minLines = 1, + maxLines = 1, modifier = Modifier .fillMaxWidth() - .testTag("PaymentRequestNote"), + .testTag("PaymentRequestNote") ) + if (recipient != null) { + VerticalSpacer(16.dp) + Caption13Up( + text = stringResource(R.string.wallet__payment_request_recipient), + color = Colors.White64, + ) + VerticalSpacer(8.dp) + PaymentRequestCard( + request = PaykitPaymentRequest( + paymentRequestId = "preview", + counterparty = recipient.publicKey, + counterpartyReceiverPath = "", + amountValue = amountState.sats.toString(), + amountSats = amountState.sats.toULong(), + note = note.trim().ifBlank { null }, + expiresAt = Clock.System.now() + expiration.duration, + acceptedPaymentEndpointIdentifiers = emptyList(), + ), + contact = recipient, + compactSubtitle = note.trim().ifBlank { recipient.name }, + ) + } VerticalSpacer(16.dp) Caption13Up( text = stringResource(R.string.wallet__payment_request_expires), @@ -256,7 +282,7 @@ internal fun PaymentRequestDetailsContent( role = Role.RadioButton selected = isSelected } - .testTag("PaymentRequestExpiry${option.name}"), + .testTag("PaymentRequestExpiry${option.name}") ) { BodyS( text = option.title(), @@ -272,8 +298,15 @@ internal fun PaymentRequestDetailsContent( } FillHeight() PrimaryButton( - text = stringResource(R.string.wallet__payment_request_choose_recipient), - enabled = amountState.sats > 0, + text = stringResource( + if (recipient != null) { + R.string.wallet__payment_request_send_request + } else { + R.string.wallet__payment_request_choose_recipient + } + ), + enabled = amountState.sats > 0 && !isCreating, + isLoading = isCreating, onClick = { onContinue( PaykitPaymentRequestDraft( @@ -283,7 +316,9 @@ internal fun PaymentRequestDetailsContent( ) ) }, - modifier = Modifier.testTag("PaymentRequestAmountContinue") + modifier = Modifier.testTag( + if (recipient != null) "PaymentRequestSend" else "PaymentRequestAmountContinue" + ) ) } VerticalSpacer(16.dp) @@ -295,22 +330,22 @@ internal fun PaymentRequestDetailsContent( @Composable fun PaymentRequestRecipientScreen( appViewModel: AppViewModel, - draft: PaykitPaymentRequestDraft, + onBack: () -> Unit, onEditExpiration: () -> Unit, - onSent: (PaykitPaymentRequest) -> Unit, + onRecipientSelected: (PaykitPaymentRequestTarget) -> Unit, ) { val context = LocalContext.current val targets by appViewModel.eligiblePaymentRequestTargets.collectAsStateWithLifecycle() val contacts by appViewModel.pubkyContacts.collectAsStateWithLifecycle() - val isCreating by appViewModel.isCreatingPaymentRequest.collectAsStateWithLifecycle() PaymentRequestRecipientContent( targets = targets.toImmutableList(), contacts = contacts.toImmutableList(), - isCreating = isCreating, + isCreating = false, + onBack = onBack, onEditExpiration = onEditExpiration, onPaste = { context.getClipboardText()?.trim().orEmpty() }, - onSend = { target -> appViewModel.createPaymentRequest(draft, target, onSent) }, + onSend = onRecipientSelected, ) } @@ -320,6 +355,7 @@ internal fun PaymentRequestRecipientContent( targets: ImmutableList, contacts: ImmutableList, isCreating: Boolean, + onBack: () -> Unit, onEditExpiration: () -> Unit, onPaste: () -> String, onSend: (PaykitPaymentRequestTarget) -> Unit, @@ -352,6 +388,7 @@ internal fun PaymentRequestRecipientContent( ) { SheetTopBar( titleText = stringResource(R.string.wallet__payment_request_choose_recipient), + onBack = onBack, action = { IconButton( onClick = onEditExpiration, @@ -386,7 +423,7 @@ internal fun PaymentRequestRecipientContent( query = PubkyPublicKeyFormat.bounded(onPaste()) } .padding(horizontal = 12.dp) - .testTag("PaymentRequestRecipientPaste"), + .testTag("PaymentRequestRecipientPaste") ) { Icon( painter = painterResource(R.drawable.ic_clipboard_text), @@ -399,7 +436,7 @@ internal fun PaymentRequestRecipientContent( }, modifier = Modifier .fillMaxWidth() - .testTag("PaymentRequestRecipientSearch"), + .testTag("PaymentRequestRecipientSearch") ) VerticalSpacer(16.dp) } @@ -424,7 +461,7 @@ internal fun PaymentRequestRecipientContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp) - .testTag("PaymentRequestRecipientUnavailable"), + .testTag("PaymentRequestRecipientUnavailable") ) } } @@ -439,7 +476,7 @@ internal fun PaymentRequestRecipientContent( isEnabled = !isCreating, modifier = Modifier .padding(horizontal = 16.dp) - .testTag("PaymentRequestContact${contact.publicKey}"), + .testTag("PaymentRequestContact${contact.publicKey}") ) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) } @@ -454,7 +491,7 @@ internal fun PaymentRequestRecipientContent( }, modifier = Modifier .padding(horizontal = 16.dp) - .testTag("PaymentRequestSend"), + .testTag("PaymentRequestSend") ) VerticalSpacer(16.dp) } @@ -483,15 +520,25 @@ internal fun PaymentRequestSentContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() - .testTag("PaymentRequestSent"), + .testTag("PaymentRequestSent") ) { SheetTopBar(titleText = stringResource(R.string.wallet__payment_request_sent_title)) - VerticalSpacer(16.dp) Column( + horizontalAlignment = Alignment.Start, modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) ) { + VerticalSpacer(32.dp) + Image( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(256.dp) + .testTag("PaymentRequestSentCheck") + ) + VerticalSpacer(32.dp) Display( text = stringResource(R.string.wallet__payment_request_sent_headline) .withAccent(accentColor = Colors.Purple), @@ -501,24 +548,18 @@ internal fun PaymentRequestSentContent( text = stringResource(R.string.wallet__payment_request_sent_description), color = Colors.White64, ) - Image( - painter = painterResource(R.drawable.check), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .testTag("PaymentRequestSentCheck"), - ) + VerticalSpacer(16.dp) PaymentRequestCard( request = request, contact = contact, - compactSubtitle = if (request.deliveryStatus == PaykitPaymentRequestDeliveryStatus.Sent) { - stringResource(R.string.wallet__payment_request_waiting) - } else { - stringResource(R.string.wallet__payment_request_sending) - }, + compactSubtitle = request.note?.takeIf { it.isNotBlank() } + ?: if (request.deliveryStatus == PaykitPaymentRequestDeliveryStatus.Sent) { + stringResource(R.string.wallet__payment_request_waiting) + } else { + stringResource(R.string.wallet__payment_request_sending) + }, ) - VerticalSpacer(16.dp) + FillHeight() PrimaryButton( text = stringResource(R.string.common__ok), onClick = onDone, @@ -577,6 +618,23 @@ private fun PaymentRequestDetailsPreview() { } } +@Preview(showSystemUi = true) +@Composable +private fun PaymentRequestDetailsContactPreview() { + AppThemeSurface { + BottomSheetPreview { + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = previewDraft, + onBack = {}, + onContinue = {}, + recipient = PubkyProfile.placeholder(previewTarget.publicKey), + modifier = Modifier.sheetHeight() + ) + } + } +} + @Preview(showSystemUi = true) @Composable private fun PaymentRequestRecipientPreview() { @@ -586,6 +644,7 @@ private fun PaymentRequestRecipientPreview() { targets = persistentListOf(previewTarget), contacts = persistentListOf(PubkyProfile.placeholder(previewTarget.publicKey)), isCreating = false, + onBack = {}, onEditExpiration = {}, onPaste = { "" }, onSend = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt index 4a7404bed5..b8ce4985dd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt @@ -157,7 +157,7 @@ internal fun PaymentRequestsSheetContent( onClick = onSeeAll, modifier = Modifier .weight(1f) - .testTag("PaymentRequestsSeeAll"), + .testTag("PaymentRequestsSeeAll") ) } VerticalSpacer(16.dp) @@ -207,7 +207,6 @@ internal fun PaymentRequestsContent( Column( modifier = modifier .fillMaxSize() - .gradientBackground() .navigationBarsPadding() .testTag("PaymentRequestsScreen") ) { @@ -220,7 +219,7 @@ internal fun PaymentRequestsContent( Column( modifier = Modifier .weight(1f) - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) ) { FillHeight() Image( @@ -229,7 +228,7 @@ internal fun PaymentRequestsContent( modifier = Modifier .size(256.dp) .align(Alignment.CenterHorizontally) - .testTag("PaymentRequestsEmptyIllustration"), + .testTag("PaymentRequestsEmptyIllustration") ) FillHeight() Display( @@ -249,23 +248,39 @@ internal fun PaymentRequestsContent( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .weight(1f) - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) ) { - if (sections.active.isNotEmpty()) { + if (sections.incoming.isNotEmpty()) { item { Caption13Up( - text = stringResource(R.string.wallet__payment_requests_section), + text = stringResource(R.string.wallet__payment_requests_incoming), color = Colors.White64, ) } - items(sections.active, key = { it.lazyListKey }) { request -> - ActivePaymentRequestCard( + items(sections.incoming, key = { it.lazyListKey }) { request -> + PaymentRequestCard( request = request, - isIncoming = pending.any { it.id == request.id }, + contact = contacts.contactFor(request), isRejecting = request.id in rejectingRequestIds, + onPay = { onPay(request.id) }, + onReject = { onReject(request) }, + ) + } + } + if (sections.outgoing.isNotEmpty()) { + item { + Caption13Up( + text = stringResource(R.string.wallet__payment_requests_outgoing), + color = Colors.White64, + ) + } + items(sections.outgoing, key = { it.lazyListKey }) { request -> + PaymentRequestCard( + request = request, contact = contacts.contactFor(request), - onPay = onPay, - onReject = onReject, + compactSubtitle = request.note?.takeIf { it.isNotBlank() } + ?: paymentRequestStatus(request), + secondaryText = stringResource(R.string.wallet__payment_request_pending), ) } } @@ -294,15 +309,15 @@ internal fun PaymentRequestsContent( onClick = onRequestPayment, modifier = Modifier .padding(horizontal = 16.dp) - .testTag("PaymentRequestCreate"), + .testTag("PaymentRequestCreate") ) - VerticalSpacer(16.dp) } } } private data class PaymentRequestSections( - val active: List, + val incoming: List, + val outgoing: List, val history: List, ) @@ -326,13 +341,14 @@ private fun paymentRequestSections( now: Instant, ): PaymentRequestSections { val pendingIds = pending.mapTo(mutableSetOf()) { it.id } - val active = requests.filter { request -> - request.id in pendingIds || - request.direction == PaykitPaymentRequestDirection.Outgoing && + val incoming = requests.filter { it.id in pendingIds } + val outgoing = requests.filter { request -> + request.direction == PaykitPaymentRequestDirection.Outgoing && request.lifecycleState == PaymentRequestLifecycleState.PROPOSED && - !request.isExpired(now) + !request.isExpired(now) && + request.id !in pendingIds } - val activeIds = active.mapTo(mutableSetOf()) { it.id } + val activeIds = (incoming + outgoing).mapTo(mutableSetOf()) { it.id } val groupedHistory = requests .filterNot { it.id in activeIds } .sortedWith { first, second -> compareValues(second.createdAt, first.createdAt) } @@ -340,36 +356,7 @@ private fun paymentRequestSections( val history = PaymentRequestHistoryPeriod.entries.mapNotNull { period -> groupedHistory[period]?.let { PaymentRequestHistorySection(period, it) } } - return PaymentRequestSections(active, history) -} - -@Composable -private fun ActivePaymentRequestCard( - request: PaykitPaymentRequest, - isIncoming: Boolean, - isRejecting: Boolean, - contact: PubkyProfile?, - onPay: (PaykitPaymentRequestId) -> Unit, - onReject: (PaykitPaymentRequest) -> Unit, -) { - if (isIncoming) { - PaymentRequestCard( - request = request, - contact = contact, - isRejecting = isRejecting, - onPay = { onPay(request.id) }, - onReject = { onReject(request) }, - ) - } else { - PaymentRequestCard( - request = request, - contact = contact, - compactSubtitle = stringResource( - R.string.wallet__payment_request_waiting_for_recipient, - contact?.name ?: PubkyProfile.placeholder(request.counterparty).name, - ), - ) - } + return PaymentRequestSections(incoming, outgoing, history) } @Composable @@ -448,6 +435,7 @@ internal fun PaymentRequestCard( request: PaykitPaymentRequest, contact: PubkyProfile?, compactSubtitle: String? = null, + secondaryText: String? = null, isRejecting: Boolean = false, onPay: (() -> Unit)? = null, onReject: (() -> Unit)? = null, @@ -476,7 +464,7 @@ internal fun PaymentRequestCard( .outerGlow( glowColor = Colors.Brand, glowOpacity = 0.16f, - glowRadius = 64.dp, + glowRadius = 16.dp, cornerRadius = 16.dp, ) .border(1.dp, Colors.Brand.copy(alpha = 0.5f), MaterialTheme.shapes.medium) @@ -484,7 +472,7 @@ internal fun PaymentRequestCard( Modifier } ) - .testTag("PaymentRequestRow${request.paymentRequestId}"), + .testTag("PaymentRequestRow${request.paymentRequestId}") ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -507,6 +495,7 @@ internal fun PaymentRequestCard( } MoneyCell( sats = request.amountSats.coerceAtMost(Long.MAX_VALUE.toULong()).toLong(), + secondaryText = secondaryText, ) } if (onPay != null || onReject != null) { @@ -515,7 +504,7 @@ internal fun PaymentRequestCard( modifier = Modifier .fillMaxWidth() .background(Colors.Gray5) - .padding(16.dp), + .padding(16.dp) ) { SecondaryButton( text = stringResource(R.string.wallet__payment_request_dismiss), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 020aba5a78..e76c5d7455 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -25,9 +25,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable import to.bitkit.R +import to.bitkit.models.PubkyProfile +import to.bitkit.models.PubkyPublicKeyFormat import to.bitkit.repositories.LightningState import to.bitkit.repositories.PaykitPaymentRequest import to.bitkit.repositories.PaykitPaymentRequestDraft +import to.bitkit.repositories.PaykitPaymentRequestTarget import to.bitkit.repositories.WalletState import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.navigateTo @@ -82,6 +85,13 @@ fun ReceiveSheet( ) } var createdPaymentRequest by remember { mutableStateOf(null) } + var paymentRequestTarget by remember { mutableStateOf(null) } + val contacts by appViewModel.pubkyContacts.collectAsStateWithLifecycle() + val isCreatingPaymentRequest by appViewModel.isCreatingPaymentRequest.collectAsStateWithLifecycle() + val paymentRequestRecipient = paymentRequestTarget?.let { target -> + contacts.firstOrNull { PubkyPublicKeyFormat.matches(it.publicKey, target.publicKey) } + ?: PubkyProfile.placeholder(target.publicKey) + } LaunchedEffect(Unit) { wallet.resetPreActivityMetadataTagsForCurrentInvoice() @@ -91,13 +101,13 @@ fun ReceiveSheet( Box( modifier = Modifier .fillMaxWidth() - .sheetHeight(), + .sheetHeight() ) { Column( modifier = Modifier .fillMaxSize() .imePadding() - .testTag("ReceiveScreen"), + .testTag("ReceiveScreen") ) { NavHost( navController = navController, @@ -127,12 +137,22 @@ fun ReceiveSheet( PaymentRequestDetailsScreen( amountInputViewModel = paymentRequestAmountViewModel, initialDraft = paymentRequestDraft, + recipient = paymentRequestRecipient, + isCreating = isCreatingPaymentRequest, onBack = { if (!navController.popBackStack()) appViewModel.hideSheet() }, - onContinue = { - paymentRequestDraft = it - navController.navigateTo(ReceiveRoute.PaymentRequestRecipient) + onContinue = { draft -> + paymentRequestDraft = draft + val target = paymentRequestTarget + if (target != null) { + appViewModel.createPaymentRequest(draft, target) { + createdPaymentRequest = it + navController.navigateToPaymentRequestSent() + } + } else { + navController.navigateTo(ReceiveRoute.PaymentRequestRecipient) + } }, ) } @@ -140,6 +160,8 @@ fun ReceiveSheet( PaymentRequestDetailsScreen( amountInputViewModel = paymentRequestAmountViewModel, initialDraft = paymentRequestDraft, + recipient = paymentRequestRecipient, + isCreating = isCreatingPaymentRequest, onBack = { navController.popBackStack() }, onContinue = { paymentRequestDraft = it @@ -150,13 +172,15 @@ fun ReceiveSheet( composableWithDefaultTransitions { PaymentRequestRecipientScreen( appViewModel = appViewModel, - draft = paymentRequestDraft, + onBack = { navController.popBackStack() }, onEditExpiration = { navController.navigateTo(ReceiveRoute.PaymentRequestExpiration) }, - onSent = { - createdPaymentRequest = it - navController.navigateToPaymentRequestSent() + onRecipientSelected = { target -> + paymentRequestTarget = target + navController.navigateTo(ReceiveRoute.PaymentRequestDetails) { + popUpTo(ReceiveRoute.PaymentRequestRecipient) { inclusive = true } + } }, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cce3f106cd..6ec1e1cdd8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1199,6 +1199,7 @@ What is this payment for? Paste Pay + pending Your payment request is queued and will send automatically RECIPIENT No matching saved contact with a private connection. @@ -1220,18 +1221,18 @@ %1$s at %2$s Waiting for payment Waiting for updated private payment details. Bitkit will retry automatically. - Waiting for %1$s to pay Payment Requests Earlier You have not made any payments to providers and don’t have any payment requests yet. History]]> + Incoming Not Now + Outgoing %1$d pending payment request %1$d pending payment requests Review each request, then pay or dismiss. - PAYMENT REQUESTS See All This Month This Week From 89a421321c33e02fdc3309bdbebb753da7cf3420 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 18:03:36 +0200 Subject: [PATCH 5/8] fix: bottom-align sent sheet content --- .../ui/screens/paymentrequests/CreatePaymentRequestScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 9737e19891..1a12e1b82c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -529,7 +529,7 @@ internal fun PaymentRequestSentContent( .fillMaxSize() .padding(horizontal = 16.dp) ) { - VerticalSpacer(32.dp) + FillHeight() Image( painter = painterResource(R.drawable.check), contentDescription = null, @@ -559,7 +559,7 @@ internal fun PaymentRequestSentContent( stringResource(R.string.wallet__payment_request_sending) }, ) - FillHeight() + VerticalSpacer(16.dp) PrimaryButton( text = stringResource(R.string.common__ok), onClick = onDone, From 8d0b4395cae31800b2189747de1145d4baf51372 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 19:03:12 +0200 Subject: [PATCH 6/8] fix: restore sent card-to-ok gap --- .../ui/screens/paymentrequests/CreatePaymentRequestScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 1a12e1b82c..ae29cac04a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -559,7 +559,7 @@ internal fun PaymentRequestSentContent( stringResource(R.string.wallet__payment_request_sending) }, ) - VerticalSpacer(16.dp) + VerticalSpacer(32.dp) PrimaryButton( text = stringResource(R.string.common__ok), onClick = onDone, From b6c49c217f9d2c58a4a6a09945700611ad0a121a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 21:13:43 +0200 Subject: [PATCH 7/8] fix: hide invoice contacts header --- .../CreatePaymentRequestScreenTest.kt | 44 +++- .../PaymentRequestFigmaFixtures.kt | 100 ++++++++ .../PaymentRequestUiCaptureTest.kt | 240 ++++++++++++++++++ .../CreatePaymentRequestScreen.kt | 17 +- .../screens/wallets/receive/ReceiveSheet.kt | 4 + 5 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestFigmaFixtures.kt create mode 100644 app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt index 39d98a5636..6e619a63eb 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput -import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test import to.bitkit.models.PubkyProfile @@ -74,32 +74,64 @@ class CreatePaymentRequestScreenTest { @Test fun recipientShowsEligibleContactAndSendAction() { + val contacts = PaymentRequestFigmaFixtures.recipientContacts composeTestRule.setContent { AppThemeSurface { PaymentRequestRecipientContent( - targets = persistentListOf(target), - contacts = persistentListOf(PubkyProfile.placeholder(target.publicKey)), + targets = PaymentRequestFigmaFixtures.recipientTargets, + contacts = contacts, isCreating = false, onBack = {}, onEditExpiration = {}, - onPaste = { target.publicKey }, + onPaste = { PaymentRequestFigmaFixtures.anna.publicKey }, onSend = {}, ) } } - composeTestRule.onNodeWithTag("PaymentRequestContact${target.publicKey}").assertIsDisplayed() + composeTestRule.onNodeWithText("Alex Stronghand").assertIsDisplayed() + composeTestRule.onNodeWithText("Anna Pleb").assertIsDisplayed() + composeTestRule.onNodeWithText("Areem Holden").assertIsDisplayed() + composeTestRule.onNodeWithText("Craig Wrong").assertIsDisplayed() + composeTestRule.onNodeWithText("John Carvalho").performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestRecipientSearch").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestContactsHeader").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestEditExpiration").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestRecipientPaste", useUnmergedTree = true).assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestRecipientSearch").performTextInput("not this contact") - composeTestRule.onNodeWithTag("PaymentRequestContact${target.publicKey}").assertDoesNotExist() + composeTestRule.onNodeWithText("Alex Stronghand").assertDoesNotExist() composeTestRule.onNodeWithText("No matching saved contact with a private connection.").assertIsDisplayed() } + @Test + fun recipientFromInvoiceHidesContactsHeader() { + composeTestRule.setContent { + AppThemeSurface { + PaymentRequestRecipientContent( + targets = PaymentRequestFigmaFixtures.recipientInvoiceTargets, + contacts = PaymentRequestFigmaFixtures.recipientInvoiceContacts, + isCreating = false, + onBack = {}, + onEditExpiration = {}, + onPaste = { PaymentRequestFigmaFixtures.anna.publicKey }, + onSend = {}, + showContactsHeader = false, + ) + } + } + + composeTestRule.onNodeWithTag("PaymentRequestContactsHeader").assertDoesNotExist() + composeTestRule.onNodeWithText("Alex Stronghand").assertIsDisplayed() + composeTestRule.onNodeWithText("Anna Pleb").assertIsDisplayed() + composeTestRule.onNodeWithText("Areem Holden").assertIsDisplayed() + composeTestRule.onNodeWithText("Craig Wrong").assertIsDisplayed() + composeTestRule.onNodeWithText("John Carvalho").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText("Paola Andina").performScrollTo().assertIsDisplayed() + } + @Test fun sentShowsSuccessSurface() { composeTestRule.setContent { diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestFigmaFixtures.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestFigmaFixtures.kt new file mode 100644 index 0000000000..94c275f743 --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestFigmaFixtures.kt @@ -0,0 +1,100 @@ +package to.bitkit.ui.screens.paymentrequests + +import com.synonym.paykit.PaymentRequestLifecycleState +import kotlinx.collections.immutable.persistentListOf +import to.bitkit.models.PubkyProfile +import to.bitkit.repositories.PaykitPaymentRequest +import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus +import to.bitkit.repositories.PaykitPaymentRequestDirection +import to.bitkit.repositories.PaykitPaymentRequestTarget +import kotlin.time.Instant + +/** Figma frames 48185:303457, 48185:303440, 48185:303376. */ +internal object PaymentRequestFigmaFixtures { + val alex = profile("pubkyyrsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Alex Stronghand") + val anna = profile("pubky1rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Anna Pleb") + val areem = profile("pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Areem Holden") + val craig = profile("pubkybrsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Craig Wrong") + val john = profile("pubky5rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "John Carvalho") + val paola = profile("pubkynrsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Paola Andina") + val ben = profile("pubky8rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg", "Ben") + + val recipientContacts = persistentListOf(alex, anna, areem, craig, john) + val recipientInvoiceContacts = persistentListOf(alex, anna, areem, craig, john, paola) + val listContacts = persistentListOf(areem, ben, anna, john) + + val recipientTargets = persistentListOf( + target(alex), + target(anna), + target(areem), + target(craig), + target(john), + ) + val recipientInvoiceTargets = persistentListOf( + target(alex), + target(anna), + target(areem), + target(craig), + target(john), + target(paola), + ) + + fun listRequests(now: Instant) = persistentListOf( + request("incoming-areem", areem.publicKey, 21_000uL, "Lunch last week", now), + request("incoming-ben", ben.publicKey, 100_000uL, "Groceries", now), + request( + id = "outgoing-anna", + counterparty = anna.publicKey, + amountSats = 14_500uL, + note = "Snacks at conference", + createdAt = now, + direction = PaykitPaymentRequestDirection.Outgoing, + deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent, + ), + request( + id = "outgoing-john", + counterparty = john.publicKey, + amountSats = 50_000uL, + note = "Steaks & Burgers", + createdAt = now, + direction = PaykitPaymentRequestDirection.Outgoing, + deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent, + ), + ) + + fun listPending(now: Instant) = persistentListOf( + request("incoming-areem", areem.publicKey, 21_000uL, "Lunch last week", now), + request("incoming-ben", ben.publicKey, 100_000uL, "Groceries", now), + ) + + fun target(profile: PubkyProfile) = PaykitPaymentRequestTarget( + publicKey = profile.publicKey, + receiverPath = "bitkit/wallet", + ) + + fun request( + id: String, + counterparty: String, + amountSats: ULong, + note: String, + createdAt: Instant, + direction: PaykitPaymentRequestDirection = PaykitPaymentRequestDirection.Incoming, + deliveryStatus: PaykitPaymentRequestDeliveryStatus? = null, + ) = PaykitPaymentRequest( + paymentRequestId = id, + counterparty = counterparty, + counterpartyReceiverPath = "bitkit/wallet", + amountValue = "0", + amountSats = amountSats, + note = note, + createdAt = createdAt, + expiresAt = null, + acceptedPaymentEndpointIdentifiers = listOf("btc-lightning-bolt11"), + deliveryStatus = deliveryStatus, + direction = direction, + lifecycleState = PaymentRequestLifecycleState.PROPOSED, + ) + + private fun profile(publicKey: String, name: String) = + PubkyProfile.forDisplay(publicKey, name, null) +} diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt new file mode 100644 index 0000000000..309fba32fc --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt @@ -0,0 +1,240 @@ +@file:OptIn(ExperimentalTime::class) + +package to.bitkit.ui.screens.paymentrequests + +import android.graphics.Bitmap +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.collections.immutable.persistentSetOf +import org.junit.Rule +import org.junit.Test +import to.bitkit.repositories.AmountInputHandler +import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus +import to.bitkit.repositories.PaykitPaymentRequestDirection +import to.bitkit.repositories.PaykitPaymentRequestDraft +import to.bitkit.test.annotations.ComposeUi +import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.viewmodels.AmountInputViewModel +import java.io.File +import kotlin.time.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.ExperimentalTime + +@ComposeUi +class PaymentRequestUiCaptureTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun captureList() { + val now = Clock.System.now() + captureScreen("list") { + PaymentRequestsContent( + requests = PaymentRequestFigmaFixtures.listRequests(now), + pending = PaymentRequestFigmaFixtures.listPending(now), + contacts = PaymentRequestFigmaFixtures.listContacts, + rejectingRequestIds = persistentSetOf(), + canRequestPayment = true, + onBack = {}, + onRequestPayment = {}, + onPay = {}, + onReject = {}, + ) + } + } + + @Test + fun captureRecipient() { + captureSheet("recipient") { modifier -> + PaymentRequestRecipientContent( + targets = PaymentRequestFigmaFixtures.recipientTargets, + contacts = PaymentRequestFigmaFixtures.recipientContacts, + isCreating = false, + onBack = {}, + onEditExpiration = {}, + onPaste = { "" }, + onSend = {}, + modifier = modifier + ) + } + } + + @Test + fun captureRecipientInvoice() { + captureSheet("recipient-invoice") { modifier -> + PaymentRequestRecipientContent( + targets = PaymentRequestFigmaFixtures.recipientInvoiceTargets, + contacts = PaymentRequestFigmaFixtures.recipientInvoiceContacts, + isCreating = false, + onBack = {}, + onEditExpiration = {}, + onPaste = { "" }, + onSend = {}, + showContactsHeader = false, + modifier = modifier + ) + } + } + + @Test + fun captureDetails() { + captureSheet("details") { modifier -> + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = PaykitPaymentRequestDraft( + amountSats = 14_500uL, + note = "Snacks at conference", + expiresAt = Clock.System.now() + 7.days, + ), + onBack = {}, + onContinue = {}, + modifier = modifier + ) + } + } + + @Test + fun captureDetailsContact() { + captureSheet("details-contact") { modifier -> + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = PaykitPaymentRequestDraft( + amountSats = 50_000uL, + note = "Steaks & Burgers", + expiresAt = Clock.System.now() + 7.days, + ), + onBack = {}, + onContinue = {}, + recipient = PaymentRequestFigmaFixtures.john, + modifier = modifier + ) + } + } + + @Test + fun captureAmountEmpty() { + setSheetContent { modifier -> + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = PaykitPaymentRequestDraft( + amountSats = 0uL, + note = "", + expiresAt = Clock.System.now() + 7.days, + ), + onBack = {}, + onContinue = {}, + modifier = modifier + ) + } + composeTestRule.onNodeWithTag("PaymentRequestEditAmount").performClick() + composeTestRule.waitForIdle() + save("amount-empty") + } + + @Test + fun captureAmountFilled() { + setSheetContent { modifier -> + PaymentRequestDetailsContent( + amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()), + initialDraft = PaykitPaymentRequestDraft( + amountSats = 50_000uL, + note = "Steaks & Burgers", + expiresAt = Clock.System.now() + 7.days, + ), + onBack = {}, + onContinue = {}, + modifier = modifier + ) + } + composeTestRule.onNodeWithTag("PaymentRequestEditAmount").performClick() + composeTestRule.waitForIdle() + save("amount-filled") + } + + @Test + fun captureSent() { + captureSheet("sent") { modifier -> + PaymentRequestSentContent( + request = PaymentRequestFigmaFixtures.request( + id = "outgoing-anna", + counterparty = PaymentRequestFigmaFixtures.anna.publicKey, + amountSats = 14_500uL, + note = "Snacks at conference", + createdAt = Clock.System.now(), + direction = PaykitPaymentRequestDirection.Outgoing, + deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent, + ), + contact = PaymentRequestFigmaFixtures.anna, + onDone = {}, + modifier = modifier + ) + } + } + + private fun captureScreen(name: String, content: @Composable () -> Unit) { + composeTestRule.setContent { + AppThemeSurface { + CompositionLocalProvider(LocalInspectionMode provides true) { + content() + } + } + } + composeTestRule.waitForIdle() + save(name) + } + + private fun captureSheet(name: String, content: @Composable (Modifier) -> Unit) { + setSheetContent(content) + composeTestRule.waitForIdle() + save(name) + } + + private fun setSheetContent(content: @Composable (Modifier) -> Unit) { + composeTestRule.setContent { + AppThemeSurface { + CompositionLocalProvider(LocalInspectionMode provides true) { + BottomSheetPreview { + content(Modifier.sheetHeight()) + } + } + } + } + composeTestRule.waitForIdle() + } + + private fun save(name: String) { + composeTestRule.waitForIdle() + val instrumentation = InstrumentationRegistry.getInstrumentation() + val bitmap = instrumentation.uiAutomation.takeScreenshot() + ?: composeTestRule.onRoot().captureToImage().asAndroidBitmap() + val context = instrumentation.targetContext + val dirs = listOfNotNull( + File("/data/local/tmp"), + context.getExternalFilesDir(null), + context.filesDir, + ) + var saved = false + dirs.forEach { dir -> + runCatching { + dir.mkdirs() + File(dir, "$name-after.png").outputStream().use { out -> + check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)) + } + saved = true + android.util.Log.e("PR_CAPTURE", "saved ${File(dir, "$name-after.png").absolutePath} w=${bitmap.width} h=${bitmap.height}") + } + } + check(saved) { "failed to save $name-after.png" } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index ae29cac04a..47d293dc43 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -333,6 +333,7 @@ fun PaymentRequestRecipientScreen( onBack: () -> Unit, onEditExpiration: () -> Unit, onRecipientSelected: (PaykitPaymentRequestTarget) -> Unit, + showContactsHeader: Boolean = true, ) { val context = LocalContext.current val targets by appViewModel.eligiblePaymentRequestTargets.collectAsStateWithLifecycle() @@ -346,6 +347,7 @@ fun PaymentRequestRecipientScreen( onEditExpiration = onEditExpiration, onPaste = { context.getClipboardText()?.trim().orEmpty() }, onSend = onRecipientSelected, + showContactsHeader = showContactsHeader, ) } @@ -359,6 +361,7 @@ internal fun PaymentRequestRecipientContent( onEditExpiration: () -> Unit, onPaste: () -> String, onSend: (PaykitPaymentRequestTarget) -> Unit, + showContactsHeader: Boolean = true, ) { var selectedTarget by remember { mutableStateOf(null) } var query by remember { mutableStateOf("") } @@ -440,11 +443,15 @@ internal fun PaymentRequestRecipientContent( ) VerticalSpacer(16.dp) } - Caption13Up( - text = stringResource(R.string.contacts__contacts_header), - color = Colors.White64, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) - ) + if (showContactsHeader) { + Caption13Up( + text = stringResource(R.string.contacts__contacts_header), + color = Colors.White64, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 16.dp) + .testTag("PaymentRequestContactsHeader") + ) + } HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) LazyColumn(modifier = Modifier.weight(1f)) { if (recipients.isEmpty()) { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index e76c5d7455..a262b33571 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -86,6 +86,7 @@ fun ReceiveSheet( } var createdPaymentRequest by remember { mutableStateOf(null) } var paymentRequestTarget by remember { mutableStateOf(null) } + var showRecipientContactsHeader by remember { mutableStateOf(true) } val contacts by appViewModel.pubkyContacts.collectAsStateWithLifecycle() val isCreatingPaymentRequest by appViewModel.isCreatingPaymentRequest.collectAsStateWithLifecycle() val paymentRequestRecipient = paymentRequestTarget?.let { target -> @@ -151,6 +152,7 @@ fun ReceiveSheet( navController.navigateToPaymentRequestSent() } } else { + showRecipientContactsHeader = true navController.navigateTo(ReceiveRoute.PaymentRequestRecipient) } }, @@ -182,6 +184,7 @@ fun ReceiveSheet( popUpTo(ReceiveRoute.PaymentRequestRecipient) { inclusive = true } } }, + showContactsHeader = showRecipientContactsHeader, ) } composableWithDefaultTransitions { @@ -296,6 +299,7 @@ fun ReceiveSheet( note = note, expiresAt = Clock.System.now() + 7.days, ) + showRecipientContactsHeader = false navController.navigateTo(ReceiveRoute.PaymentRequestRecipient) }, navigateReceiveConfirm = { entry -> From b519d4c8332470fb8a7c8b16d3311632ded3fc92 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 22:31:50 +0200 Subject: [PATCH 8/8] test: dark host behind recipient capture --- .../PaymentRequestUiCaptureTest.kt | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt index 309fba32fc..ddb46d7f2f 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestUiCaptureTest.kt @@ -3,13 +3,18 @@ package to.bitkit.ui.screens.paymentrequests import android.graphics.Bitmap +import android.graphics.Color as AndroidColor +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.test.captureToImage -import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick @@ -25,6 +30,7 @@ import to.bitkit.test.annotations.ComposeUi import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.AmountInputViewModel import java.io.File import kotlin.time.Clock @@ -34,7 +40,7 @@ import kotlin.time.ExperimentalTime @ComposeUi class PaymentRequestUiCaptureTest { @get:Rule - val composeTestRule = createComposeRule() + val composeTestRule = createAndroidComposeRule() @Test fun captureList() { @@ -183,10 +189,19 @@ class PaymentRequestUiCaptureTest { } private fun captureScreen(name: String, content: @Composable () -> Unit) { + composeTestRule.runOnUiThread { + composeTestRule.activity.window.decorView.setBackgroundColor(AndroidColor.BLACK) + } composeTestRule.setContent { AppThemeSurface { CompositionLocalProvider(LocalInspectionMode provides true) { - content() + Box( + modifier = Modifier + .fillMaxSize() + .background(Colors.Black), + ) { + content() + } } } } @@ -201,11 +216,20 @@ class PaymentRequestUiCaptureTest { } private fun setSheetContent(content: @Composable (Modifier) -> Unit) { + composeTestRule.runOnUiThread { + composeTestRule.activity.window.decorView.setBackgroundColor(AndroidColor.BLACK) + } composeTestRule.setContent { AppThemeSurface { CompositionLocalProvider(LocalInspectionMode provides true) { - BottomSheetPreview { - content(Modifier.sheetHeight()) + Box( + modifier = Modifier + .fillMaxSize() + .background(Colors.Black), + ) { + BottomSheetPreview { + content(Modifier.sheetHeight()) + } } } }