diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt index e6fb2d2460..4127284871 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt @@ -54,6 +54,8 @@ import to.bitkit.R import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType +import to.bitkit.models.PaykitAmount +import to.bitkit.models.PaykitAsset import to.bitkit.repositories.UsdtWalletState import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BiometricsView @@ -135,6 +137,12 @@ fun UsdtWalletScreen( var maximumAmount by rememberSaveable { mutableStateOf(null) } val destinations by viewModel.destinations.collectAsStateWithLifecycle() var destination by rememberSaveable { mutableStateOf(UsdtDestination.ARBITRUM) } + InitialUsdtQuoteEffect( + payment = InitialUsdtPayment(page, recipient, amount, destination, hasQuote = state.quote != null), + initialRecipient = initialRecipient, + initialAmount = initialAmount, + onQuote = { viewModel.quote(recipient, amount, destination) }, + ) val dismissalBlocked = state.busy || state.authenticationRequired val dismiss = { if (!dismissalBlocked) { @@ -299,6 +307,35 @@ fun UsdtWalletScreen( } } +@Composable +private fun InitialUsdtQuoteEffect( + payment: InitialUsdtPayment, + initialRecipient: String, + initialAmount: String, + onQuote: () -> Unit, +) { + var isRequested by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + if (isRequested) return@LaunchedEffect + isRequested = true + val isInitialPayment = payment.page == UsdtPage.AMOUNT && payment.recipient == initialRecipient && + payment.amount == initialAmount && payment.destination == UsdtDestination.ARBITRUM && !payment.hasQuote + if (isInitialPayment && shouldQuoteInitialUsdtPayment(initialRecipient, initialAmount)) onQuote() + } +} + +private data class InitialUsdtPayment( + val page: UsdtPage, + val recipient: String, + val amount: String, + val destination: UsdtDestination, + val hasQuote: Boolean, +) + +/** A payment request with a recipient and a positive amount opens on its review, as tapping Review would. */ +internal fun shouldQuoteInitialUsdtPayment(recipient: String, amount: String): Boolean = + recipient.isNotEmpty() && runCatching { PaykitAmount.parseInput(PaykitAsset.USDT, amount) }.isSuccess + @Composable internal fun rememberUsdtPage(initialRecipient: String, state: UsdtSendState): MutableState { val paymentInProgress = state.submitted || (state.busy && state.quote != null) diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 7906998ac3..54e76cb77e 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -129,6 +129,23 @@ internal fun NavController.navigateToSendAmount(startDestination: SendRoute) { } } +/** Confirm opened straight from an invoice amount is replaced by Amount, so Back from Amount skips it. */ +internal fun NavController.navigateToAmountFromConfirm() { + navigateTo(SendRoute.Amount) { + popUpTo { inclusive = true } + } +} + +/** Back from Confirm, shared by the toolbar arrow and system Back. Returns false when the sheet should close. */ +internal fun NavController.backFromConfirm(canReturnToAmount: Boolean): Boolean { + if (popBackStack(SendRoute.Amount, inclusive = false)) return true + if (canReturnToAmount) { + navigateToAmountFromConfirm() + return true + } + return popBackStack() +} + internal fun NavController.navigateToSendPending(route: SendRoute.Pending) { navigateTo(route) { popUpTo(graph.id) { inclusive = true } } } @@ -491,6 +508,12 @@ fun SendSheet( initialSubscriptionId?.let(appViewModel::subscription) } + val onConfirmBack = { + if (!navController.backFromConfirm(uiState.canReturnToAmount)) appViewModel.hideSheet() + } + // Confirm can be the sheet's first screen, where the nav host does not handle system Back. + BackHandler(enabled = uiState.canReturnToAmount, onBack = onConfirmBack) + SendConfirmScreen( savedStateHandle = it.savedStateHandle, uiState = uiState, @@ -498,13 +521,8 @@ fun SendSheet( lightningState.nodeLifecycleState.isRunning(), canAutoStart = !isOffline && !shouldShowSyncOverlay, syncNodeVariant = syncNodeVariant, - canGoBack = navController.previousBackStackEntry != null, - onBack = { - val didPopToAmount = navController.popBackStack(SendRoute.Amount, inclusive = false) - if (!didPopToAmount && !navController.popBackStack()) { - appViewModel.hideSheet() - } - }, + canGoBack = navController.previousBackStackEntry != null || uiState.canReturnToAmount, + onBack = onConfirmBack, onEvent = { e -> appViewModel.setSendEvent(e) }, onClickAddTag = { navController.navigateTo(SendRoute.AddTag) }, onClickTag = { tag -> appViewModel.removeTag(tag) }, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 95bcf05879..e7c31ef29b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -5399,6 +5399,8 @@ class AppViewModel @Inject constructor( return } + if (amount > 0u && openOnchainConfirmForInvoiceAmount(amount, fromMainScanner, walletGeneration)) return + Logger.info( when (amount > 0u) { true -> "Found amount in invoice, proceeding to edit amount" @@ -5410,6 +5412,30 @@ class AppViewModel @Inject constructor( navigateToSendRoute(fromMainScanner, SendRoute.Amount, SendEffect.NavigateToAmount, walletGeneration) } + /** + * An on-chain invoice with a payable amount opens on Confirm, as Amount's continue would. + * Manual coin selection and contact payments keep going through Amount. + */ + private suspend fun openOnchainConfirmForInvoiceAmount( + amount: ULong, + fromMainScanner: Boolean, + walletGeneration: Long?, + ): Boolean { + if (_sendUiState.value.payMethod != SendMethod.ONCHAIN || hasActiveContactPaymentContext()) return false + if (!validateAmount(amount)) return false + val isHardware = _sendUiState.value.hardwareWalletId != null + if (!isHardware && !settingsStore.data.first().coinSelectAuto) return false + requireCurrentPaymentPreparation(walletGeneration) + if (isHardware && !prepareHardwareSendFee()) return false + requireCurrentPaymentPreparation(walletGeneration) + + Logger.info("Found payable amount in invoice, proceeding to confirm", context = TAG) + _sendUiState.update { it.copy(isAmountInputValid = true, canReturnToAmount = true) } + navigateToSendRoute(fromMainScanner, SendRoute.Confirm, SendEffect.NavigateToConfirm, walletGeneration) + refreshOnchainSendIfNeeded(walletGeneration) + return true + } + private suspend fun hardwareMaxSpendable( walletId: String, address: String, @@ -8935,6 +8961,7 @@ data class SendUiState( val isInitialSubscriptionPayment: Boolean = false, val initialSubscriptionPaymentAutoStartPending: Boolean = false, val requiresAmountReview: Boolean = false, + val canReturnToAmount: Boolean = false, val initialSubscriptionPaymentNeedsConfirmation: Boolean = false, val incomingPaymentRequestId: PaykitPaymentRequestId? = null, ) { diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreenTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreenTest.kt new file mode 100644 index 0000000000..04bdba14bd --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreenTest.kt @@ -0,0 +1,28 @@ +package to.bitkit.ui.screens.wallets.usdt + +import org.junit.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class UsdtWalletScreenTest { + private val recipient = "0x1111111111111111111111111111111111111111" + + @Test + fun `initial payment with recipient and positive amount is quoted`() { + assertTrue(shouldQuoteInitialUsdtPayment(recipient, "0.010001")) + assertTrue(shouldQuoteInitialUsdtPayment(recipient, "5")) + } + + @Test + fun `initial payment without a payable amount is not quoted`() { + assertFalse(shouldQuoteInitialUsdtPayment(recipient, "")) + assertFalse(shouldQuoteInitialUsdtPayment(recipient, "0")) + assertFalse(shouldQuoteInitialUsdtPayment(recipient, "abc")) + assertFalse(shouldQuoteInitialUsdtPayment(recipient, "0.0000001")) + } + + @Test + fun `initial payment without recipient is not quoted`() { + assertFalse(shouldQuoteInitialUsdtPayment("", "5")) + } +} diff --git a/app/src/test/java/to/bitkit/ui/sheets/SendSheetNavigationTest.kt b/app/src/test/java/to/bitkit/ui/sheets/SendSheetNavigationTest.kt index eb65547d98..80c4efd71e 100644 --- a/app/src/test/java/to/bitkit/ui/sheets/SendSheetNavigationTest.kt +++ b/app/src/test/java/to/bitkit/ui/sheets/SendSheetNavigationTest.kt @@ -12,6 +12,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import to.bitkit.ui.navigateTo +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -47,6 +48,51 @@ class SendSheetNavigationTest { } } + @Test + fun `back from a confirm opened by an invoice amount replaces it with amount`() { + for (start in listOf(SendRoute.Confirm, SendRoute.Recipient)) { + val navController = NavHostController(ApplicationProvider.getApplicationContext()).apply { + navigatorProvider.addNavigator(ComposeNavigator()) + graph = createGraph(startDestination = start) { + composable {} + composable {} + composable {} + } + } + if (start != SendRoute.Confirm) navController.navigateTo(SendRoute.Confirm) + + navController.navigateToAmountFromConfirm() + + assertTrue(requireNotNull(navController.currentDestination).hasRoute()) + val previous = navController.previousBackStackEntry?.destination + if (start == SendRoute.Confirm) { + assertNull(previous) + } else { + assertTrue(requireNotNull(previous).hasRoute()) + } + navController.navigateTo(SendRoute.Confirm) + assertTrue(navController.popBackStack(SendRoute.Amount, inclusive = false)) + } + } + + @Test + fun `back from a first confirm opens amount only when it can return to amount`() { + for (canReturnToAmount in listOf(true, false)) { + val navController = NavHostController(ApplicationProvider.getApplicationContext()).apply { + navigatorProvider.addNavigator(ComposeNavigator()) + graph = createGraph(startDestination = SendRoute.Confirm) { + composable {} + composable {} + } + } + + val handled = navController.backFromConfirm(canReturnToAmount) + + assertEquals(canReturnToAmount, handled) + assertEquals(canReturnToAmount, navController.currentDestination?.hasRoute() == true) + } + } + @Test fun `pending clears earlier send screens after prepared amount review`() { for (start in listOf(SendRoute.Confirm, SendRoute.ContactSelect, SendRoute.Amount)) { diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 89de855cf8..a3ed611382 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -118,6 +118,7 @@ import to.bitkit.data.keychain.hasPendingWalletRestore import to.bitkit.domain.commands.NotifyChannelReadyHandler import to.bitkit.domain.commands.NotifyPaymentReceived import to.bitkit.domain.commands.NotifyPaymentReceivedHandler +import to.bitkit.env.Defaults import to.bitkit.env.Env import to.bitkit.ext.callbackAmountMsats import to.bitkit.ext.fromHex @@ -9329,6 +9330,89 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(Sheet.Send(SendRoute.Amount), sut.currentSheet.value) } + @Test + fun `main scanner onchain scan with payable amount opens confirm sheet`() = test { + val input = stubOnchainAmountScan(amountSats = 10_000u) + + sut.showScannerSheet() + sut.onScannerSheetResult(input) + advanceUntilIdle() + + assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) + val state = sut.sendUiState.value + assertEquals(10_000uL, state.amount) + assertEquals(SendMethod.ONCHAIN, state.payMethod) + assertTrue(state.isAmountInputValid) + assertTrue(state.canReturnToAmount) + } + + @Test + fun `main scanner onchain scan without amount opens amount sheet`() = test { + val input = stubOnchainAmountScan(amountSats = 0u) + + sut.showScannerSheet() + sut.onScannerSheetResult(input) + advanceUntilIdle() + + assertEquals(Sheet.Send(SendRoute.Amount), sut.currentSheet.value) + assertFalse(sut.sendUiState.value.canReturnToAmount) + } + + @Test + fun `main scanner onchain scan with dust amount opens amount sheet`() = test { + val input = stubOnchainAmountScan(amountSats = Defaults.dustLimit.toULong()) + + sut.showScannerSheet() + sut.onScannerSheetResult(input) + advanceUntilIdle() + + assertEquals(Sheet.Send(SendRoute.Amount), sut.currentSheet.value) + assertFalse(sut.sendUiState.value.canReturnToAmount) + } + + @Test + fun `main scanner onchain scan with amount opens amount sheet for manual coin selection`() = test { + settingsData.update { it.copy(coinSelectAuto = false) } + val input = stubOnchainAmountScan(amountSats = 10_000u) + + sut.showScannerSheet() + sut.onScannerSheetResult(input) + advanceUntilIdle() + + assertEquals(Sheet.Send(SendRoute.Amount), sut.currentSheet.value) + assertEquals(10_000uL, sut.sendUiState.value.amount) + assertFalse(sut.sendUiState.value.canReturnToAmount) + } + + @Test + fun `send sheet onchain address with payable amount navigates to confirm`() = test { + val input = stubOnchainAmountScan(amountSats = 10_000u) + + sut.showSheet(Sheet.Send()) + advanceUntilIdle() + sut.sendEffect.test { + sut.setSendEvent(SendEvent.AddressContinue(input)) + advanceUntilIdle() + + assertEquals(SendEffect.NavigateToConfirm, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + assertTrue(sut.sendUiState.value.canReturnToAmount) + } + + private suspend fun stubOnchainAmountScan(amountSats: ULong): String { + whenever(coreService.validateBitcoinAddress(REGTEST_ADDRESS)).thenReturn( + ValidationResult(REGTEST_ADDRESS, NetworkType.REGTEST, AddressType.P2WPKH), + ) + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u) + sut.setIsAuthenticated(true) + val input = "bitcoin:$REGTEST_ADDRESS?amount=$amountSats" + whenever(coreService.decode(input)).thenReturn( + Scanner.OnChain(OnChainInvoice(REGTEST_ADDRESS, amountSats, null, null, null)), + ) + return input + } + @Test fun `onchain validation mismatch is silent after wallet replacement`() = test { val input = "bitcoin:$REGTEST_ADDRESS?amount=0.0001" @@ -16732,6 +16816,47 @@ class AppViewModelSendFlowTest : BaseUnitTest() { verify(paykitPaymentProofRepo, never()).prepare(any(), any(), any(), any(), anyOrNull(), anyOrNull()) } + @Test + fun `fixed dollar request accepting USDT opens bitcoin confirmation while USDT is off`() = test { + enablePaykitUi() + usdtEnabled.value = false + pubkyPublicKey.value = testPublicKey + sut.setIsAuthenticated(true) + runCurrent() + val endpoints = listOf( + Endpoint(MethodId.P2wpkh, REGTEST_ADDRESS, rawPayload = REGTEST_ADDRESS), + Endpoint(MethodId.UsdtArbitrum, "0x1111111111111111111111111111111111111111", rawPayload = ""), + ) + val request = paymentRequest().copy( + amount = PaykitAmount(PaykitAsset.USD, 250u), + pricing = PaykitRequestPricing( + PaymentConversion.Fixed(listOf(ConversionRate("btc", "0.00001"), ConversionRate("usdt", "1"))), + ), + acceptedPaymentEndpointIdentifiers = endpoints.map { it.methodId.rawValue }, + ) + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u) + whenever(coreService.decode(REGTEST_ADDRESS)).thenReturn( + Scanner.OnChain(OnChainInvoice(REGTEST_ADDRESS, 0u, null, null, null)), + ) + val resolution = CompletableDeferred>() + whenever(privatePaykitRepo.beginPaymentRequest(request)).doSuspendableAnswer { resolution.await() } + pendingPaykitPaymentRequests.value = listOf(request) + runCurrent() + assertEquals(request, (sut.currentSheet.value as? Sheet.Send)?.preparingRequest) + resolution.complete( + Result.success(PublicPaykitPaymentResult.Opened(REGTEST_ADDRESS, endpoints = endpoints)) + ) + runCurrent() + + assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) + val state = sut.sendUiState.value + assertFalse(state.paykitUsesUsdt) + assertEquals(SendMethod.ONCHAIN, state.payMethod) + assertEquals(2_500uL, state.amount) + assertTrue(state.isAmountInputValid) + assertFalse(state.shouldConfirmPay) + } + @Test fun `quoted Bitcoin wallet switches stay on confirmation and bind the new amount`() = test { usdtEnabled.value = false diff --git a/changelog.d/next/1763.fixed.md b/changelog.d/next/1763.fixed.md new file mode 100644 index 0000000000..eb7c18b264 --- /dev/null +++ b/changelog.d/next/1763.fixed.md @@ -0,0 +1 @@ +Scanning a Bitcoin or USDT payment request that already has an amount now opens the review screen directly instead of the amount screen. diff --git a/journeys/boost/rbf-own-send.xml b/journeys/boost/rbf-own-send.xml index c28e37792c..8b533ce09c 100644 --- a/journeys/boost/rbf-own-send.xml +++ b/journeys/boost/rbf-own-send.xml @@ -7,8 +7,8 @@ the configured regtest backend's block-mining helper. - Hand the app the address as a URI with adb shell am start -a android.intent.action.VIEW -d "bitcoin:ADDRESS?amount=0.0001" to.bitkit.dev, which opens the amount screen at 10,000 sats - Verify the amount screen shows 10,000 sats, tap Continue (testTag "ContinueAmount") and verify the review screen (testTag "SendConfirm") is visible + Hand the app the address as a URI with adb shell am start -a android.intent.action.VIEW -d "bitcoin:ADDRESS?amount=0.0001" to.bitkit.dev, which opens the review screen at 10,000 sats + Verify the review screen (title "Confirm") is visible and its amount (testTag "ReviewAmount") shows 10,000 sats Swipe the send handle (testTag "GRAB") fully to the right and wait for the success screen (testTag "SendSuccess"), then tap Close Tap the newest outgoing activity entry (testTag "ActivityShort-0") and verify it shows 10,000 sats; record the fee from testTag "ActivityFee" Tap Transaction details (testTag "ActivityTxDetails"), record the TXID, then go back diff --git a/journeys/payment-requests/README.md b/journeys/payment-requests/README.md index 00b162d346..548bf24d3c 100644 --- a/journeys/payment-requests/README.md +++ b/journeys/payment-requests/README.md @@ -5,8 +5,8 @@ Cover incoming Paykit Payment Requests from a linked issuer. The issuer contract [`app/src/test/resources/paykit-issuer-interoperability.json`](../../app/src/test/resources/paykit-issuer-interoperability.json). `fixed-price-bitcoin.xml` verifies a USD-denominated request settled in BTC and an explicit -same-asset rail price. After preparation, the Amount screen shows the fixed, uneditable payment -amount with Savings selected. Continue opens confirmation with the same amount. The amount uses +same-asset rail price. After preparation, confirmation opens directly with the fixed, uneditable +payment amount, without an Amount screen in between. The amount uses the issuer's fixed rate; the displayed fiat estimate uses Bitkit's own market rate. The flow stops before broadcasting. Differing rail prices, fractional-satoshi Lightning payments, and dynamic quotes remain unsupported. diff --git a/journeys/payment-requests/fixed-price-bitcoin.xml b/journeys/payment-requests/fixed-price-bitcoin.xml index 138c2a1cae..b5f842d551 100644 --- a/journeys/payment-requests/fixed-price-bitcoin.xml +++ b/journeys/payment-requests/fixed-price-bitcoin.xml @@ -6,18 +6,16 @@ Launch the E2E Bitkit app with Paykit UI enabled and the fixture issuer saved as a contact and linked as identities Have the issuer publish a current regtest P2WPKH address under identifier "btc-regtest-p2wpkh" with JSON payload {"value":"<current address>"} Have the issuer send a new one-time request for amount "10", asset "usd", accepted identifier "btc-regtest-p2wpkh", and fixed conversion rate "btc" equal to "0.000021" - Wait for request preparation to finish and the Amount screen to open with Savings selected + Wait for request preparation to finish and verify the Payment Request confirmation screen (id "PaymentRequestConfirm") opens directly, without the Amount screen (id "SendNumberField") or its keypad being shown Switch the amount display to Bitcoin if needed - Verify the requested amount is 21,000 sats, cannot be edited, and does not show 10 BTC - Tap Continue and verify the Payment Request confirmation screen (id "PaymentRequestConfirm") shows 21,000 sats + Verify the confirmation amount (id "ReviewAmount") is 21,000 sats, cannot be edited, and does not show 10 BTC Wait for fresh validation and wallet preparation to finish; verify the same sheet stays open and its payment swipe control (id "GRAB") stops showing progress Verify the fiat estimate equals Bitkit's current local valuation of 21,000 sats, rather than being forced to the issuer's 10 USD denomination Dismiss the Payment Request confirmation screen without paying Have the issuer send a new one-time request for amount "0.001", asset "btc", accepted identifier "btc-regtest-p2wpkh", and fixed rates "btc" equal to "2" and "btc-regtest" equal to "0.5" - Wait for request preparation to finish and the Amount screen to open with Savings selected + Wait for request preparation to finish and verify the Payment Request confirmation screen (id "PaymentRequestConfirm") opens directly, without the Amount screen (id "SendNumberField") being shown Switch the amount display to Bitcoin if needed - Verify the requested amount is 50,000 sats, cannot be edited, and does not show 100,000 or 200,000 sats - Tap Continue and verify the Payment Request confirmation screen (id "PaymentRequestConfirm") still shows 50,000 sats + Verify the confirmation amount (id "ReviewAmount") is 50,000 sats, cannot be edited, and does not show 100,000 or 200,000 sats Dismiss the Payment Request confirmation screen without paying diff --git a/journeys/payment-requests/fixed-quoted-confirmation.xml b/journeys/payment-requests/fixed-quoted-confirmation.xml index b5bfa029bc..fe4a0e0294 100644 --- a/journeys/payment-requests/fixed-quoted-confirmation.xml +++ b/journeys/payment-requests/fixed-quoted-confirmation.xml @@ -3,6 +3,7 @@ Disable USDT on the payer. Create a fixed 2.50 USD request accepting Bitcoin, then open it on the payer. Verify Preparing changes directly to Confirm without showing a disabled Amount keypad. Verify the exact BTC amount follows the request quote, the fiat equivalent uses the payer wallet rate, and the fee is shown before payment is enabled. + With USDT still disabled, open a fixed 2.50 USD request accepting both Bitcoin and USDT. Verify Preparing again changes directly to the Bitcoin Confirm without showing the Amount screen or an Enable USDT prompt. Close without paying. Choose a non-default automatic coin-selection setting, open a fixed quoted Savings request and verify the selected coins and fee follow that setting. Close, choose manual coin selection and reopen; verify the coin picker appears before payment can be approved. Go Back without selecting coins and verify Confirm stays open, allowing a funding change. Swipe again and verify coin selection is still required. Close without paying. Verify the requested amount cannot be edited and payment does not start without the confirmation swipe. Close the sheet and verify the unpaid request remains available to reopen. Reopen the same request, then close while Preparing is visible. Verify late completion does not reopen the sheet or start a payment. Reopen once more and verify confirmation is usable. diff --git a/journeys/pubky-auth/signin-with-second-link.xml b/journeys/pubky-auth/signin-with-second-link.xml index edfa81aa39..c66c94d88b 100644 --- a/journeys/pubky-auth/signin-with-second-link.xml +++ b/journeys/pubky-auth/signin-with-second-link.xml @@ -13,22 +13,22 @@ Verify the PIN pad is visible (testTag "PinPad") Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:bcrt1qh6k3khs7qz04a9p5eam6v48nlug9d38x4rgxrn?amount=0.0001" to.bitkit.dev Enter the PIN with the number pad keys - Verify the authorization sheet is visible (testTag "PubkyAuthAuthorize") and the Send amount screen is not shown + Verify the authorization sheet is visible (testTag "PubkyAuthAuthorize") and the Send Confirm screen is not shown Press back to close the authorization sheet - Verify the Send amount screen opens (testTag "SendNumberField") showing 10 000 sats, then press back until the wallet home screen is visible + Verify the Send Confirm screen opens (title "Confirm", amount testTag "ReviewAmount") showing 10 000 sats, then press back until the wallet home screen is visible Run adb shell am force-stop to.bitkit.dev Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:bcrt1qh6k3khs7qz04a9p5eam6v48nlug9d38x4rgxrn?amount=0.0001" to.bitkit.dev Verify the PIN pad is visible (testTag "PinPad") Run adb shell am start -a android.intent.action.VIEW -d "<a fresh signin_grant URL>" to.bitkit.dev Enter the PIN with the number pad keys - Verify the authorization sheet is visible (testTag "PubkyAuthAuthorize") and the Send amount screen is not shown + Verify the authorization sheet is visible (testTag "PubkyAuthAuthorize") and the Send Confirm screen is not shown Press back to close the authorization sheet - Verify the Send amount screen opens (testTag "SendNumberField") showing 10 000 sats, then press back until the wallet home screen is visible + Verify the Send Confirm screen opens (title "Confirm", amount testTag "ReviewAmount") showing 10 000 sats, then press back until the wallet home screen is visible Run adb shell am start -a android.intent.action.VIEW -d "<a fresh signin_grant URL>" to.bitkit.dev Verify the authorization sheet is visible (testTag "PubkyAuthAuthorize") Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:bcrt1qh6k3khs7qz04a9p5eam6v48nlug9d38x4rgxrn?amount=0.0001" to.bitkit.dev - Verify the authorization sheet is still visible (testTag "PubkyAuthAuthorize") and the Send amount screen is not shown + Verify the authorization sheet is still visible (testTag "PubkyAuthAuthorize") and the Send Confirm screen is not shown Press back to close the authorization sheet - Verify the Send amount screen opens (testTag "SendNumberField") showing 10 000 sats + Verify the Send Confirm screen opens (title "Confirm", amount testTag "ReviewAmount") showing 10 000 sats diff --git a/journeys/security/pin-lock-on-resume.xml b/journeys/security/pin-lock-on-resume.xml index b124b62535..e66771c807 100644 --- a/journeys/security/pin-lock-on-resume.xml +++ b/journeys/security/pin-lock-on-resume.xml @@ -42,7 +42,7 @@ Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:bcrt1qh6k3khs7qz04a9p5eam6v48nlug9d38x4rgxrn?amount=0.0001" to.bitkit.dev Verify the PIN pad is visible and the Send amount screen is not shown Enter the PIN with the number pad keys - Verify the Send amount screen opens (testTag "SendNumberField") showing 10 000 sats and the Receive sheet (testTag "ReceiveScreen") is gone + Verify the Send Confirm screen opens (title "Confirm", amount testTag "ReviewAmount") showing 10 000 sats and the Receive sheet (testTag "ReceiveScreen") is gone Press back to close the Send sheet and verify the wallet home screen is visible diff --git a/journeys/send/onchain-accepted-result.xml b/journeys/send/onchain-accepted-result.xml index 9d18c74341..3aaff7b22e 100644 --- a/journeys/send/onchain-accepted-result.xml +++ b/journeys/send/onchain-accepted-result.xml @@ -10,8 +10,7 @@ Hand the receiving test wallet address to the app with adb shell am start -a android.intent.action.VIEW -d "bitcoin:<test recipient address>?amount=0.00001" to.bitkit.dev - Verify the Send Amount screen is visible with 1 000 sats and Savings selected - Tap Continue (tag "ContinueAmount"), expand Show Details (tag "SendConfirmToggleDetails") if collapsed, and verify the Confirm screen shows the receiving test wallet address (tag "ReviewUri") + Verify the Confirm screen opens directly with 1 000 sats (tag "ReviewAmount"), expand Show Details (tag "SendConfirmToggleDetails") if collapsed, and verify it shows Savings and the receiving test wallet address (tag "ReviewUri") Swipe the send handle (tag "GRAB") fully to the right Wait for the Bitcoin Sent success screen (tag "SendSuccess") and verify it shows the accepted payment amount Tap Details (tag "Details"), then Explore (tag "ActivityTxDetails"), and verify the outbound transaction shows a transaction ID (tag "TXID"); record that exact ID without inferring a different transaction from address or amount diff --git a/journeys/send/scan-onchain-amount-to-confirm.xml b/journeys/send/scan-onchain-amount-to-confirm.xml new file mode 100644 index 0000000000..441a36a139 --- /dev/null +++ b/journeys/send/scan-onchain-amount-to-confirm.xml @@ -0,0 +1,31 @@ + + + Verifies that an on-chain BIP21 request with an amount the Savings balance covers opens the + Confirm screen directly, with the amount, recipient and fee filled in, and that Back from Confirm + opens Amount with the same amount so it can still be edited. Nothing is broadcast. + Precondition: an onboarded dev wallet with between 200 000 and 1 000 000 confirmed sats in + Savings, funded through the lsp regtest deposit and mine (see ../README.md), Coin Selection set + to Autopilot (the default), no hardware wallet selected, and a regtest address the wallet does + not own. Manual coin selection keeps going through Amount and then the coin selection screen; + see ../coin-selection/manual-coin-selection.xml. Start on the wallet home screen. + The Confirm screen is identified by its "Confirm" title and the amount (testTag "ReviewAmount"); + the Send Amount screen by its number field (testTag "SendNumberField"). + + + Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:<regtest address>?amount=0.0001" to.bitkit.dev + Verify the Confirm screen opens without the Send Amount screen (testTag "SendNumberField") having been shown, and the amount (testTag "ReviewAmount") shows 10 000, waiting through "Connecting to network" if it is shown + Tap Show Details (testTag "SendConfirmToggleDetails") and verify the recipient (testTag "ReviewUri") is the regtest address and Speed and Fee shows a fee + Tap Back (testTag "NavigationBack") + Verify the Send Amount screen (testTag "SendNumberField") is visible with 10 000 sats + Tap Continue (testTag "ContinueAmount") and verify the Confirm screen shows 10 000 again + Tap Back (testTag "NavigationBack") and verify the Send Amount screen is visible, then tap Back (testTag "NavigationBack") and verify the wallet home screen is visible without sending + Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:<regtest address>?amount=0.0001" to.bitkit.dev and verify the Confirm screen shows 10 000 + Run adb shell input keyevent KEYCODE_BACK and verify the Send Amount screen (testTag "SendNumberField") is visible with 10 000 sats rather than the sheet closing + Run adb shell input keyevent KEYCODE_BACK and verify the Send sheet closes without sending + Open Send (testTag "Send"), choose Enter Manually (testTag "RecipientManual"), enter "bitcoin:<regtest address>?amount=0.0001" into the recipient field (testTag "RecipientInput") and tap Continue (testTag "AddressContinue") + Verify the Confirm screen opens directly showing 10 000 + Tap Back (testTag "NavigationBack") and verify the Send Amount screen shows 10 000, then tap Back (testTag "NavigationBack") and verify the recipient field still holds the URI; close the sheet without sending + Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:<regtest address>?amount=0.01" to.bitkit.dev + Verify the Insufficient Savings toast appears (screenshot) and neither the Send Amount nor the Confirm screen opens + + diff --git a/journeys/send/scan-onchain-no-amount-to-amount.xml b/journeys/send/scan-onchain-no-amount-to-amount.xml new file mode 100644 index 0000000000..728807df87 --- /dev/null +++ b/journeys/send/scan-onchain-no-amount-to-amount.xml @@ -0,0 +1,18 @@ + + + Verifies that an on-chain BIP21 request without an amount, or with a zero amount, still opens + the Send Amount screen with an empty amount, so the user enters one before Confirm. + Precondition: an onboarded dev wallet with confirmed sats in Savings, funded through the lsp + regtest deposit and mine (see ../README.md), and a regtest address the wallet does not own. + Start on the wallet home screen. Nothing is broadcast. + + + Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:<regtest address>" to.bitkit.dev + Verify the Send Amount screen (testTag "SendNumberField") is visible with an amount of 0 + Enter 10000 with the number pad keys (testTags "N1", "N0") and tap Continue (testTag "ContinueAmount") + Verify the Confirm screen opens with the amount (testTag "ReviewAmount") showing 10 000 + Tap Back (testTag "NavigationBack") and verify the Send Amount screen shows 10 000, then close the sheet without sending + Run adb shell am start -a android.intent.action.VIEW -d "bitcoin:<regtest address>?amount=0" to.bitkit.dev + Verify the Send Amount screen (testTag "SendNumberField") is visible with an amount of 0, then close the sheet without sending + + diff --git a/journeys/usdt-payment-links-and-scans.xml b/journeys/usdt-payment-links-and-scans.xml index a9751a1bde..4308667b7d 100644 --- a/journeys/usdt-payment-links-and-scans.xml +++ b/journeys/usdt-payment-links-and-scans.xml @@ -2,7 +2,7 @@ Use an onboarded wallet with USDT configured and another owned wallet's Arbitrum receive URI. No payment is submitted. Hardware-wallet send remains Bitcoin-only. A full-balance quote can fail at balance validation or paymaster fee collection; either error can occur on either platform, depending on which validation fails first. Both must reject before signing. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. - Open an ethereum: USDT receive link with chain 42161, the pinned Arbitrum token, an owned recipient and uint256=10001. Verify the USDT amount screen preserves the recipient and exact input 0.010001 USDT; the review rounds its displayed value to $0.01. Verify no payment occurs without approval. + Open an ethereum: USDT receive link with chain 42161, the pinned Arbitrum token, an owned recipient and uint256=10001. Verify the link opens on payment review without tapping Review, preserving the recipient and exact input 0.010001 USDT; the review rounds its displayed value to $0.01. With a USDT balance below the amount plus fees, verify the amount screen stays open with the same insufficient-balance or fee error that tapping Review shows. Go back to the recipient and continue again; verify the review is not requested again until Review is tapped. Verify no payment occurs without approval. Copy the USDT receive URI, return to Home and accept the clipboard prompt. Verify the USDT amount screen opens with the copied recipient and amount. Cancel without paying. Repeat with a wrong-chain URI and verify no payment prompt appears. Cancel the send and remain on the USDT wallet without returning Home, then reopen the identical URI. Verify the amount or review screen opens again. Cancel back to the USDT wallet and open a different owned recipient and amount, verifying only the new values appear. Close the send sheet, open Settings from the drawer, then go back. Verify the dismissed link does not reopen its Amount screen. Explicitly reopen the same link and verify a new send flow is available, then cancel without paying.