From f36e353fccdefa715dfd2857ff1f79a5bfb1daf1 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 1 Oct 2026 08:39:31 -0500 Subject: [PATCH 1/2] fix: present requests and keep payment controls visible --- .../ui/onboarding/MnemonicInputFieldTest.kt | 1 + .../wallets/send/SendConfirmScreenTest.kt | 58 +++++++++++- .../screens/wallets/send/SendConfirmScreen.kt | 94 ++++++++++--------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 24 +++-- .../viewmodels/AppViewModelSendFlowTest.kt | 64 +++++++++++++ changelog.d/next/1405.fixed.md | 1 + journeys/payment-requests/README.md | 2 + .../automatic-presentation.xml | 20 ++++ .../confirmation-controls.xml | 17 ++++ journeys/payment-requests/request-summary.xml | 4 +- 10 files changed, 229 insertions(+), 56 deletions(-) create mode 100644 changelog.d/next/1405.fixed.md create mode 100644 journeys/payment-requests/automatic-presentation.xml create mode 100644 journeys/payment-requests/confirmation-controls.xml diff --git a/app/src/androidTest/java/to/bitkit/ui/onboarding/MnemonicInputFieldTest.kt b/app/src/androidTest/java/to/bitkit/ui/onboarding/MnemonicInputFieldTest.kt index 77da7be4ae..8df160fece 100644 --- a/app/src/androidTest/java/to/bitkit/ui/onboarding/MnemonicInputFieldTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/onboarding/MnemonicInputFieldTest.kt @@ -61,6 +61,7 @@ class MnemonicInputFieldTest { onPositionChange = {}, onBackspaceInEmpty = { backspaceInEmptyCount++ }, focusRequester = focusRequester, + isFocused = false, index = 0, ) } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt index 20e55fd28d..6ed51789e8 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt @@ -1,12 +1,23 @@ package to.bitkit.ui.screens.wallets.send +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.getUnclippedBoundsInRoot 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.performTouchInput +import androidx.compose.ui.test.swipeUp +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import to.bitkit.models.FeeRate @@ -48,6 +59,51 @@ class SendConfirmScreenTest { composeTestRule.onNodeWithTag("SendConfirmAssetButton").assertIsDisplayed() composeTestRule.onNodeWithText("422", substring = true).assertIsDisplayed() composeTestRule.onNodeWithTag("SendConfirmToggleDetails").assertIsDisplayed() - composeTestRule.onNodeWithText("Swipe To Subscribe & Pay").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText("Swipe To Subscribe & Pay").assertIsDisplayed() + } + + @Test + fun paymentRequestKeepsAmountAndSwipeVisibleOnCompactScreen() { + val state = SendUiState( + amount = 1_753u, + payMethod = SendMethod.ONCHAIN, + isAmountInputValid = true, + isPaymentRequest = true, + paymentRequestNote = "A long invoice note. ".repeat(30), + onchainFeeUi = OnchainFeeUi(rate = FeeRate.NORMAL, sats = 141), + ) + composeTestRule.setContent { + AppThemeSurface { + CompositionLocalProvider( + LocalInspectionMode provides true, + LocalDensity provides Density(LocalDensity.current.density, fontScale = 1.3f), + ) { + Box(modifier = Modifier.size(width = 360.dp, height = 520.dp)) { + SendConfirmContent( + uiState = state, + isNodeRunning = true, + isLoading = false, + showBiometrics = false, + ) + } + } + } + } + + val amount = composeTestRule.onNodeWithTag("ReviewAmount") + val swipe = composeTestRule.onNodeWithTag("GRAB") + amount.assertIsDisplayed() + swipe.assertIsDisplayed() + val amountBounds = amount.getUnclippedBoundsInRoot() + val swipeBounds = swipe.getUnclippedBoundsInRoot() + + composeTestRule.onNodeWithTag("SendConfirmToggleDetails").performClick() + composeTestRule.onNodeWithTag("PaymentRequestInvoiceNote").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag("SendConfirmContent").performTouchInput { swipeUp() } + + amount.assertIsDisplayed() + swipe.assertIsDisplayed() + assertEquals(amountBounds, amount.getUnclippedBoundsInRoot()) + assertEquals(swipeBounds, swipe.getUnclippedBoundsInRoot()) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index d7bf38d175..b127e76937 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -332,7 +332,6 @@ private fun ContentRunning( modifier = modifier .padding(horizontal = 16.dp) .fillMaxSize() - .verticalScroll(rememberScrollState()) ) { BalanceHeaderView( sats = uiState.amount.toLong(), @@ -344,55 +343,62 @@ private fun ContentRunning( .testTag("ReviewAmount") ) - VerticalSpacer(if (uiState.isOneOffPaymentRequest && !isLnurlPay) 24.dp else 44.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .then(if (showDetails || isLnurlPay) Modifier.verticalScroll(rememberScrollState()) else Modifier) + .testTag("SendConfirmContent") + ) { + VerticalSpacer(if (uiState.isOneOffPaymentRequest && !isLnurlPay) 24.dp else 44.dp) - if (isLnurlPay) { - LnurlPayDetails(uiState = uiState, onEvent = onEvent) - } else if (showDetails) { - when (uiState.payMethod) { - SendMethod.ONCHAIN -> { - OnChainDetails( - uiState = uiState, - interactionsEnabled = !isHardwareFeeLoading, - onEvent = onEvent, - ) - VerticalSpacer(16.dp) - TagsSection(uiState, onClickTag, onClickAddTag) - uiState.oneOffPaymentRequestNote?.let { + if (isLnurlPay) { + LnurlPayDetails(uiState = uiState, onEvent = onEvent) + } else if (showDetails) { + when (uiState.payMethod) { + SendMethod.ONCHAIN -> { + OnChainDetails( + uiState = uiState, + interactionsEnabled = !isHardwareFeeLoading, + onEvent = onEvent, + ) VerticalSpacer(16.dp) - PaymentRequestInvoiceNote(note = it) + TagsSection(uiState, onClickTag, onClickAddTag) + uiState.oneOffPaymentRequestNote?.let { + VerticalSpacer(16.dp) + PaymentRequestInvoiceNote(note = it) + } } - } - SendMethod.LIGHTNING -> { - LightningDetails( - uiState = uiState, - onEvent = onEvent, - onClickTag = onClickTag, - onClickAddTag = onClickAddTag, - ) + SendMethod.LIGHTNING -> { + LightningDetails( + uiState = uiState, + onEvent = onEvent, + onClickTag = onClickTag, + onClickAddTag = onClickAddTag, + ) + } } + } else { + if (uiState.isOneOffPaymentRequest) { + PaymentRequestSummary(uiState = uiState, iconColor = accentColor) + VerticalSpacer(16.dp) + } + Image( + painter = painterResource(R.drawable.coin_stack_4), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth(IMAGE_FILL_PERCENTAGE) + .weight(1f) + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp) + .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } + ) } - } else { - if (uiState.isOneOffPaymentRequest) { - PaymentRequestSummary(uiState = uiState, iconColor = accentColor) - VerticalSpacer(16.dp) - } - Image( - painter = painterResource(R.drawable.coin_stack_4), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxWidth(IMAGE_FILL_PERCENTAGE) - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp) - .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } - ) } if (!isLnurlPay) { - FillHeight(min = 16.dp) - PrimaryButton( text = stringResource( if (showDetails) R.string.common__hide_details else R.string.common__show_details @@ -423,12 +429,10 @@ private fun ContentRunning( .align(Alignment.CenterHorizontally) .testTag("SendConfirmToggleDetails") ) - - VerticalSpacer(62.dp) - } else { - FillHeight(min = 16.dp) } + VerticalSpacer(16.dp) + SwipeToConfirm( text = stringResource( if (uiState.isInitialSubscriptionPayment) { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 473605d250..9d70a988d1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -970,13 +970,21 @@ class AppViewModel @Inject constructor( viewModelScope.launch { paykitPaymentRequestRepo.pendingRequests.drop(1).collect { requests -> retainPaymentRequestPresentationState(requests) - val activeRequest = activeIncomingPaymentRequest() ?: return@collect - if (isSubmittingPaymentRequest || uncertainOnchainPaymentRequestId == activeRequest.id) return@collect + val activeRequest = activeIncomingPaymentRequest() if ( - currentSheet.value is Sheet.Send && - requests.none { it.id == activeRequest.id } + activeRequest != null && + !isSubmittingPaymentRequest && + uncertainOnchainPaymentRequestId != activeRequest.id ) { - hideSheet() + if (currentSheet.value is Sheet.Send && requests.none { it.id == activeRequest.id }) { + hideSheet() + } + } + if ( + isPaykitEnabled.value && paymentRequestIdentity != null && + PubkyPublicKeyFormat.matches(paymentRequestIdentity, pubkyRepo.publicKey.value) + ) { + presentNextIncomingPaykitPaymentRequest() } } } @@ -1028,10 +1036,10 @@ class AppViewModel @Inject constructor( activePaymentRequestPresentationGeneration = null } + if (isPaymentRequestPresentationBlocked()) return if ( - stopped && - generation != paymentRequestPresentationGeneration && - !isPaymentRequestPresentationBlocked() + stopped && generation != paymentRequestPresentationGeneration || + paymentRequestsForPresentation()?.any { next -> requests.none { it.id == next.id } } == true ) { presentNextIncomingPaykitPaymentRequest() } diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index a8d49e8e82..e1971da6d7 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -923,6 +923,70 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) } + @Test + fun `new pending request opens without another refresh or resume`() = test { + enablePaykitUi() + pubkyPublicKey.value = testPublicKey + sut.setIsAuthenticated(true) + runCurrent() + val request = paymentRequest() + val bolt11 = "lnbcrt1newpendingrequest" + whenever(privatePaykitRepo.beginPaymentRequest(request)).thenReturn( + Result.success( + PublicPaykitPaymentResult.Opened( + paymentRequest = bolt11, + privatePaymentContext = PrivatePaykitPaymentContext("bitkit/server", 8uL), + ), + ), + ) + stubLightningScan(bolt11 = bolt11, amountSats = 0u) + balanceState.value = BalanceState(maxSendLightningSats = 100_000u) + clearInvocations(paykitPaymentRequestRepo) + + pendingPaykitPaymentRequests.value = listOf(request) + runCurrent() + + assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) + assertEquals(request.id, sut.sendUiState.value.incomingPaymentRequestId) + verify(privatePaykitRepo).beginPaymentRequest(request) + verify(paykitPaymentRequestRepo, never()).refresh() + } + + @Test + fun `request arriving during another presentation is not left waiting for refresh`() = test { + enablePaykitUi() + pubkyPublicKey.value = testPublicKey + sut.setIsAuthenticated(true) + sut.showPaymentRequests() + runCurrent() + val firstRequest = paymentRequest() + val nextRequest = paymentRequest().copy(paymentRequestId = "next-request") + val firstResolutionStarted = CompletableDeferred() + val finishFirstResolution = CompletableDeferred() + whenever(privatePaykitRepo.beginPaymentRequest(firstRequest)).doSuspendableAnswer { + firstResolutionStarted.complete(Unit) + finishFirstResolution.await() + Result.success(PublicPaykitPaymentResult.WaitingForUpdatedPaymentList) + } + val bolt11 = "lnbcrt1nextpendingrequest" + stubOpenedPaymentRequest(nextRequest, bolt11) + stubLightningScan(bolt11 = bolt11, amountSats = 0u) + balanceState.value = BalanceState(maxSendLightningSats = 100_000u) + pendingPaykitPaymentRequests.value = listOf(firstRequest) + runCurrent() + sut.hideSheet() + firstResolutionStarted.await() + + pendingPaykitPaymentRequests.value = listOf(firstRequest, nextRequest) + runCurrent() + finishFirstResolution.complete(Unit) + runCurrent() + + assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) + assertEquals(nextRequest.id, sut.sendUiState.value.incomingPaymentRequestId) + verify(privatePaykitRepo).beginPaymentRequest(nextRequest) + } + @Test fun `opened request passes its note to the confirm sheet`() = test { sut.setIsAuthenticated(true) diff --git a/changelog.d/next/1405.fixed.md b/changelog.d/next/1405.fixed.md new file mode 100644 index 0000000000..e76358f56e --- /dev/null +++ b/changelog.d/next/1405.fixed.md @@ -0,0 +1 @@ +New incoming Payment Requests open automatically while Bitkit is unlocked. The payment amount and confirmation slider stay visible while details scroll. diff --git a/journeys/payment-requests/README.md b/journeys/payment-requests/README.md index 52cd3b856f..f6c0b4074f 100644 --- a/journeys/payment-requests/README.md +++ b/journeys/payment-requests/README.md @@ -70,3 +70,5 @@ request id, check its full row identifier, and include the required back and tab transitions. The accepted subscription must have no end date so cancellation is available. The proposal review must explain that its payment details are unsupported and offer no Subscribe control. + +`automatic-presentation.xml` uses the same two-wallet setup and leaves the payer in the foreground. `confirmation-controls.xml` checks Android's fixed amount and confirmation footer on a compact screen with large text; it is not ported to iOS because the confirmation layout is different there. diff --git a/journeys/payment-requests/automatic-presentation.xml b/journeys/payment-requests/automatic-presentation.xml new file mode 100644 index 0000000000..47b5009cdd --- /dev/null +++ b/journeys/payment-requests/automatic-presentation.xml @@ -0,0 +1,20 @@ + + + Verifies that a newly received request opens automatically in the foreground, waits for another sheet to close, and does not reopen a request the payer already reviewed. Requires two Bitkit instances saved as each other's contacts and linked on receiver path "bitkit/wallet", with the payer funded for 21,000 sats; see README.md. + + + On the payer, leave Bitkit unlocked and in the foreground on Home + On the requester, send the payer contact a Payment Request for 21,000 sats with the note "First request" + On the payer, verify Payment Request confirmation (testTag "PaymentRequestConfirm") appears automatically with 21,000 sats and For shows "First request" + Close the confirmation without paying + On the payer, open Receive and leave its sheet open + On the requester, send the payer contact another Payment Request for 5,000 sats with the note "Second request" + Wait for the requester's send operation to finish and for the payer to synchronize its pending requests + On the payer, verify Receive stays open without being replaced by confirmation + Close Receive + Verify Payment Request confirmation (testTag "PaymentRequestConfirm") appears automatically with 5,000 sats and For shows "Second request" + Close the confirmation without paying + Open Receive and close it again + Verify Home remains visible and neither reviewed request reopens automatically + + diff --git a/journeys/payment-requests/confirmation-controls.xml b/journeys/payment-requests/confirmation-controls.xml new file mode 100644 index 0000000000..feccef3073 --- /dev/null +++ b/journeys/payment-requests/confirmation-controls.xml @@ -0,0 +1,17 @@ + + + Verifies that the amount and Swipe To Pay remain visible while confirmation details scroll on a compact portrait screen with large text. Requires a linked requester and a payer funded for 1,753 sats plus fees. Configure the payer at 360 dp width and 1.3 font scale before opening Bitkit. This journey covers Android's confirmation layout; iOS has a separate layout. + + + On the requester, send the payer contact a Payment Request for 1,753 sats with a multi-paragraph invoice note + On the payer, verify Payment Request confirmation (testTag "PaymentRequestConfirm") opens automatically + Verify the full 1,753-sat amount (testTag "ReviewAmount"), Show details and Swipe To Pay (testTag "GRAB") are visible without scrolling + Tap Show details (testTag "SendConfirmToggleDetails") + Scroll the middle confirmation content (testTag "SendConfirmContent") to the invoice note (testTag "PaymentRequestInvoiceNote") + Verify the full amount and Swipe To Pay remain visible in their original positions above the system navigation area + Scroll the details back to the top + Verify the full amount, Hide details and Swipe To Pay remain visible + Tap Hide details and verify the amount and Swipe To Pay remain visible + Close the confirmation without paying + + diff --git a/journeys/payment-requests/request-summary.xml b/journeys/payment-requests/request-summary.xml index 8259e8b5a7..c4da618fdf 100644 --- a/journeys/payment-requests/request-summary.xml +++ b/journeys/payment-requests/request-summary.xml @@ -4,7 +4,7 @@ On the requester, send the payer contact a Payment Request for 21,000 sats with the note "Lunch last week" - On the payer, verify the Payment Request confirmation screen (testTag "PaymentRequestConfirm") appears with 21,000 sats + On the payer, verify the Payment Request confirmation screen (testTag "PaymentRequestConfirm") appears with 21,000 sats without opening the pending Payment Requests bell Verify From (testTag "PaymentRequestFrom") shows the requester's contact name Verify For (testTag "PaymentRequestFor") shows "Lunch last week" Tap Show details (testTag "SendConfirmToggleDetails") @@ -12,7 +12,7 @@ Verify the invoice note (testTag "PaymentRequestInvoiceNote") shows "Lunch last week" Close the Payment Request confirmation screen without paying On the requester, send the payer contact a Payment Request for 5,000 sats with no note - On the payer, verify the Payment Request confirmation screen (testTag "PaymentRequestConfirm") shows 5,000 sats, opening it from the pending Payment Requests bell (testTag "PaymentRequestsBell") if it does not open by itself + On the payer, verify the Payment Request confirmation screen (testTag "PaymentRequestConfirm") appears automatically with 5,000 sats Verify From (testTag "PaymentRequestFrom") shows the requester's contact name and For (testTag "PaymentRequestFor") shows "Not specified" From 5985d70ba69eb950c2b4402fd8b85a6339451c2b Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 1 Oct 2026 09:18:20 -0500 Subject: [PATCH 2/2] fix: address request presentation edge cases --- .../wallets/send/SendConfirmScreenTest.kt | 16 +++- .../screens/wallets/send/SendConfirmScreen.kt | 90 ++++++++++--------- .../confirmation-controls.xml | 3 +- 3 files changed, 65 insertions(+), 44 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt index 6ed51789e8..9b33631f3d 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt @@ -18,9 +18,11 @@ import androidx.compose.ui.test.swipeUp import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import kotlin.test.assertEquals +import kotlin.test.assertTrue import org.junit.Rule import org.junit.Test import to.bitkit.models.FeeRate +import to.bitkit.models.PubkyProfile import to.bitkit.test.annotations.ComposeUi import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.viewmodels.OnchainFeeUi @@ -69,7 +71,8 @@ class SendConfirmScreenTest { payMethod = SendMethod.ONCHAIN, isAmountInputValid = true, isPaymentRequest = true, - paymentRequestNote = "A long invoice note. ".repeat(30), + contactPaymentProfile = PubkyProfile.placeholder("requester").copy(name = "Popup requester"), + paymentRequestNote = "A long invoice note. ".repeat(13).take(256), onchainFeeUi = OnchainFeeUi(rate = FeeRate.NORMAL, sats = 141), ) composeTestRule.setContent { @@ -78,7 +81,7 @@ class SendConfirmScreenTest { LocalInspectionMode provides true, LocalDensity provides Density(LocalDensity.current.density, fontScale = 1.3f), ) { - Box(modifier = Modifier.size(width = 360.dp, height = 520.dp)) { + Box(modifier = Modifier.size(width = 360.dp, height = 400.dp)) { SendConfirmContent( uiState = state, isNodeRunning = true, @@ -97,6 +100,15 @@ class SendConfirmScreenTest { val amountBounds = amount.getUnclippedBoundsInRoot() val swipeBounds = swipe.getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag("SendConfirmContent").getUnclippedBoundsInRoot() + composeTestRule.onNodeWithTag("PaymentRequestFrom").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestFor").performScrollTo().assertIsDisplayed() + for (tag in listOf("PaymentRequestFrom", "PaymentRequestFor")) { + val bounds = composeTestRule.onNodeWithTag(tag).getUnclippedBoundsInRoot() + assertTrue(bounds.top >= contentBounds.top) + assertTrue(bounds.bottom <= contentBounds.bottom) + } + composeTestRule.onNodeWithTag("SendConfirmToggleDetails").performClick() composeTestRule.onNodeWithTag("PaymentRequestInvoiceNote").performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("SendConfirmContent").performTouchInput { swipeUp() } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index b127e76937..44a83033ff 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -343,58 +345,64 @@ private fun ContentRunning( .testTag("ReviewAmount") ) - Column( + BoxWithConstraints( modifier = Modifier .fillMaxWidth() .weight(1f) - .then(if (showDetails || isLnurlPay) Modifier.verticalScroll(rememberScrollState()) else Modifier) .testTag("SendConfirmContent") ) { - VerticalSpacer(if (uiState.isOneOffPaymentRequest && !isLnurlPay) 24.dp else 44.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + ) { + VerticalSpacer(if (uiState.isOneOffPaymentRequest && !isLnurlPay) 24.dp else 44.dp) - if (isLnurlPay) { - LnurlPayDetails(uiState = uiState, onEvent = onEvent) - } else if (showDetails) { - when (uiState.payMethod) { - SendMethod.ONCHAIN -> { - OnChainDetails( - uiState = uiState, - interactionsEnabled = !isHardwareFeeLoading, - onEvent = onEvent, - ) - VerticalSpacer(16.dp) - TagsSection(uiState, onClickTag, onClickAddTag) - uiState.oneOffPaymentRequestNote?.let { + if (isLnurlPay) { + LnurlPayDetails(uiState = uiState, onEvent = onEvent) + } else if (showDetails) { + when (uiState.payMethod) { + SendMethod.ONCHAIN -> { + OnChainDetails( + uiState = uiState, + interactionsEnabled = !isHardwareFeeLoading, + onEvent = onEvent, + ) VerticalSpacer(16.dp) - PaymentRequestInvoiceNote(note = it) + TagsSection(uiState, onClickTag, onClickAddTag) + uiState.oneOffPaymentRequestNote?.let { + VerticalSpacer(16.dp) + PaymentRequestInvoiceNote(note = it) + } } - } - SendMethod.LIGHTNING -> { - LightningDetails( - uiState = uiState, - onEvent = onEvent, - onClickTag = onClickTag, - onClickAddTag = onClickAddTag, - ) + SendMethod.LIGHTNING -> { + LightningDetails( + uiState = uiState, + onEvent = onEvent, + onClickTag = onClickTag, + onClickAddTag = onClickAddTag, + ) + } } + } else { + if (uiState.isOneOffPaymentRequest) { + PaymentRequestSummary(uiState = uiState, iconColor = accentColor) + VerticalSpacer(16.dp) + } + Image( + painter = painterResource(R.drawable.coin_stack_4), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth(IMAGE_FILL_PERCENTAGE) + .weight(1f) + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp) + .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } + ) } - } else { - if (uiState.isOneOffPaymentRequest) { - PaymentRequestSummary(uiState = uiState, iconColor = accentColor) - VerticalSpacer(16.dp) - } - Image( - painter = painterResource(R.drawable.coin_stack_4), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxWidth(IMAGE_FILL_PERCENTAGE) - .weight(1f) - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp) - .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } - ) } } diff --git a/journeys/payment-requests/confirmation-controls.xml b/journeys/payment-requests/confirmation-controls.xml index feccef3073..16750d258b 100644 --- a/journeys/payment-requests/confirmation-controls.xml +++ b/journeys/payment-requests/confirmation-controls.xml @@ -1,11 +1,12 @@ - Verifies that the amount and Swipe To Pay remain visible while confirmation details scroll on a compact portrait screen with large text. Requires a linked requester and a payer funded for 1,753 sats plus fees. Configure the payer at 360 dp width and 1.3 font scale before opening Bitkit. This journey covers Android's confirmation layout; iOS has a separate layout. + Verifies that the amount and Swipe To Pay remain visible while the requester summary and confirmation details scroll on a compact screen with large text. Requires a linked requester and a payer funded for 1,753 sats plus fees. Configure the payer at 360 dp width and 1.3 font scale before opening Bitkit. This journey covers Android's confirmation layout; iOS has a separate layout. On the requester, send the payer contact a Payment Request for 1,753 sats with a multi-paragraph invoice note On the payer, verify Payment Request confirmation (testTag "PaymentRequestConfirm") opens automatically Verify the full 1,753-sat amount (testTag "ReviewAmount"), Show details and Swipe To Pay (testTag "GRAB") are visible without scrolling + Verify From and For remain reachable by scrolling the middle content (testTag "SendConfirmContent") if the requester summary does not fit Tap Show details (testTag "SendConfirmToggleDetails") Scroll the middle confirmation content (testTag "SendConfirmContent") to the invoice note (testTag "PaymentRequestInvoiceNote") Verify the full amount and Swipe To Pay remain visible in their original positions above the system navigation area