diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt index 1073575317..ffa580875f 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt @@ -27,6 +27,8 @@ import to.bitkit.ui.shared.util.primaryButtonStyle import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds @Composable fun NumberPadActionButton( @@ -41,6 +43,7 @@ fun NumberPadActionButton( @DrawableRes icon: Int? = null, @DrawableRes trailingIcon: Int? = null, textStyle: TextStyle = AppTextStyles.CaptionM, + debounce: Duration = 500.milliseconds, ) { val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 5.dp) val height = 28.dp @@ -60,7 +63,7 @@ fun NumberPadActionButton( } ) .animateContentSize(animationSpec = tween(durationMillis = 200)) - .clickableAlpha(enabled = enabled && clickable && !isLoading, onClick = onClick) + .clickableAlpha(enabled = enabled && clickable && !isLoading, debounce = debounce, onClick = onClick) .padding(contentPadding) ) { if (isLoading) { @@ -74,7 +77,7 @@ fun NumberPadActionButton( } else if (icon != null) { Icon( painter = painterResource(icon), - contentDescription = text, + contentDescription = null, tint = color, modifier = Modifier.size(16.dp) ) @@ -109,7 +112,7 @@ fun NumberPadActionButton( if (icon != null) { Icon( painter = painterResource(icon), - contentDescription = text, + contentDescription = null, tint = color, modifier = Modifier.size(16.dp) ) 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 6a5bc97dce..e9d3a7a60d 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 @@ -210,7 +210,7 @@ internal fun PaymentRequestAmountContent( ) } VerticalSpacer(16.dp) - PaymentRequestAssetSelector( + PaymentRequestDenominationToggle( asset = asset, amount = entered, allowedAssets = allowedAssets, @@ -282,7 +282,7 @@ internal fun PaymentRequestAmountContent( private const val MAX_USD_INPUT_LENGTH = 18 @Composable -private fun PaymentRequestAssetSelector( +private fun PaymentRequestDenominationToggle( asset: PaykitAsset, amount: PaykitAmount?, onConverted: (PaykitAmount) -> Unit, @@ -291,28 +291,27 @@ private fun PaymentRequestAssetSelector( conversion: PaykitAmountConversion, ) { val currencies = LocalCurrencies.current - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - allowedAssets.forEach { option -> - NumberPadActionButton( - text = option.code.uppercase(), - outlined = asset != option, - color = if (option == PaykitAsset.BTC) Colors.Brand else Colors.Usdt, - modifier = Modifier.testTag("PaymentRequestAsset${option.name}"), - onClick = { - if (option != asset) { - runCatching { - conversion.convert( - amount, - option, - currencies.paykitRate, - Clock.System.now().toEpochMilliseconds() - ) - }.onSuccess(onConverted).onFailure { onUnavailable() } - } - }, - ) - } - } + val nextAsset = allowedAssets.firstOrNull { it != asset } + NumberPadActionButton( + text = asset.code.uppercase(), + icon = if (nextAsset != null) R.drawable.ic_transfer else null, + color = Colors.Brand, + clickable = nextAsset != null, + debounce = Duration.ZERO, + onClick = { + if (nextAsset != null) { + runCatching { + conversion.convert( + amount, + nextAsset, + currencies.paykitRate, + Clock.System.now().toEpochMilliseconds() + ) + }.onSuccess(onConverted).onFailure { onUnavailable() } + } + }, + modifier = Modifier.testTag("PaymentRequestDenominationToggle") + ) } @Composable diff --git a/changelog.d/next/1654.fixed.md b/changelog.d/next/1654.fixed.md new file mode 100644 index 0000000000..3e3090d5a9 --- /dev/null +++ b/changelog.d/next/1654.fixed.md @@ -0,0 +1 @@ +Use a single BTC/USD denomination toggle when creating payment requests and subscriptions. diff --git a/journeys/README.md b/journeys/README.md index 059e46249e..0fc192123e 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -223,11 +223,15 @@ subscription payment-method switches: preserve the originally entered denominati ### Paykit payment-method review -In `paykit-quoted-usdt.xml`, Android returns to Amount whenever a quoted request changes payment -method. iOS keeps confirmation open when the exact amount, quote and validity window are unchanged -and still valid; changed or expired terms return to Amount. Verify the selected method and exact -payable amount on both platforms, and close without signing. The optional Savings/Spending -confirmation check requires existing Bitcoin regtest funds and a usable Lightning channel. +In `paykit-quoted-usdt.xml`, Android keeps confirmation open when a Savings/Spending switch can +bind valid quoted Bitcoin terms, showing the selected method's exact amount and refreshed fees. +Unavailable or expired terms, or a Lightning amount below its minimum, return to Amount for review. +Opening Amount manually after a valid switch and then tapping Back closes the sheet. These checks +complement `payment-requests/fixed-quoted-confirmation.xml`. iOS keeps confirmation open when the +exact amount, quote and validity window are unchanged and still valid; changed or expired terms +return to Amount. Verify the selected method and exact payable amount on both platforms, and close +without signing. The optional Savings/Spending confirmation check requires existing Bitcoin +regtest funds and a usable Lightning channel. ### USDT full-balance errors diff --git a/journeys/paykit-quoted-usdt.xml b/journeys/paykit-quoted-usdt.xml index 5ad7207fa3..45edcd32a3 100644 --- a/journeys/paykit-quoted-usdt.xml +++ b/journeys/paykit-quoted-usdt.xml @@ -1,19 +1,19 @@ - Two linked Bitkit wallets with Paykit and USDT enabled, sharing enabled by their owners, and a fresh BTC/USD rate. Request-creation and amount-screen checks need no funds. The optional Savings/Spending confirmation check needs existing Bitcoin regtest funds and a usable Lightning channel; close without signing. Android reopens Amount after a quoted request changes method; iOS keeps confirmation open when the exact payment terms remain valid and unchanged. Funded Arbitrum review, paid history, controlled rates and proof verification are manual tests in Docs/USDT-QA.md; a Bitcoin regtest balance does not fund USDT. + Two linked Bitkit wallets with Paykit and USDT enabled, sharing enabled by their owners, and a fresh BTC/USD rate. Request-creation and amount-screen checks need no funds. The optional Savings/Spending confirmation check needs existing Bitcoin regtest funds and a usable Lightning channel; close without signing. Android keeps confirmation open when a Savings/Spending switch can bind valid quoted Bitcoin terms; iOS does so when the exact payment terms remain valid and unchanged. Funded Arbitrum review, paid history, controlled rates and proof verification are manual tests in Docs/USDT-QA.md; a Bitcoin regtest balance does not fund USDT. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. - Open a linked contact and create a USD payment request. Verify BTC and USD are the denomination choices and enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with separate compact outlined Savings, Spending and USDT chips for available methods. Savings is orange, Spending is purple and USDT is teal; selected chips have a trailing X. Unselected chips retain their asset color at 50% opacity and show a trailing +. Remove one available method and verify the other selections remain unchanged; tap it again to restore it. With long translated method names on a narrow screen, verify the chips wrap onto another row without clipping their labels. + Open a linked contact and create a USD payment request. Verify a single dark filled denomination control (id "PaymentRequestDenominationToggle") shows USD with orange text and a swap icon; tapping it changes the label to BTC and tapping again restores USD. There are no separate BTC and USD denomination buttons. Verify enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with separate compact outlined Savings, Spending and USDT chips for available methods. Savings is orange, Spending is purple and USDT is teal; selected chips have a trailing X. Unselected chips retain their asset color at 50% opacity and show a trailing +. Remove one available method and verify the other selections remain unchanged; tap it again to restore it. With long translated method names on a narrow screen, verify the chips wrap onto another row without clipping their labels. On a comma-decimal device locale, enter 150,50 USD while creating a payment request. Verify the keypad, entered amount, request review and recipient row all show exactly 150,50 USD. Go back and verify the amount and denomination are preserved. On iOS with Classic Bitcoin display, repeat with a BTC amount and verify the keypad and amount use a dot and returning from review preserves the amount. - Return to the request amount editor, select USD and replace the amount with 10. Switch to BTC and back to USD four times without editing. + Return to the request amount editor, select USD and replace the amount with 10. Tap the denomination control (id "PaymentRequestDenominationToggle") to switch to BTC and back to USD four times without editing. Verify every return to USD shows exactly 10. While the BTC/USD rate is unchanged, each return to BTC must show the same satoshi amount. If the rate refreshes between switches, the BTC equivalent may change, but returning to USD must still restore exactly 10. Clear the USD input, then select BTC. Verify the amount is zero and Continue is disabled. Select USD and enter 5 followed by the decimal separator without a fractional digit. Verify Continue is enabled. Continue and verify the request review shows 5,00 USD on a comma-decimal locale or 5.00 USD on a dot-decimal locale. Deselect all receiving choices and verify sending the request is disabled. Select only USDT, enter 150,50 USD on the comma-decimal locale, select an expiry and send the request. Verify the sent screen, outgoing request card and request details all show 150,50 USD. Reopen the amount editor on a new request and verify typed trailing zeros are preserved. On the payer using the same comma-decimal locale, verify the incoming request card and details show 150,50 USD. Open the request and verify Amount represents exactly 150.50 USDT with USDT selected as the payment method, the amount cannot be edited, and no Bitcoin funding source is offered. Verify the title matches direct USDT Send and does not say Bitcoin Amount, including in German. The smaller BTC amount is a wallet-rate equivalent, not a Bitcoin payment option. Close without continuing to review. Reopen the unpaid USDT request. If Preparing is shown, verify its icons and disabled swipe use USDT teal rather than Bitcoin orange. Verify Preparing changes directly to Amount without briefly showing a Bitcoin confirmation, zero BTC or an enabled Swipe To Pay. Tap the top-bar Back arrow and verify the send sheet closes without showing a Bitcoin confirmation or a Swipe To Pay control. Reopen the same request and verify its amount and USDT payment method are preserved. Close without paying. - Create a USD request accepting Bitcoin and USDT whose quoted USDT amount is greater than the payer's USDT balance. On the payer, open the request and verify Bitcoin starts selected; USDT remains available for manual selection. Close without continuing. Repeat with a request whose quoted USDT amount is covered by that balance and verify USDT starts selected. This initial choice covers the requested amount only; fees are checked when preparing payment. When both Savings and Spending are accepted and existing regtest funds cover the payment and fees, continue with Bitcoin to confirmation and change between them. On Android, verify Amount opens again for review; tap its top-bar Back arrow and verify the sheet closes without returning to the previous confirmation. On iOS, unchanged valid payment terms keep confirmation open: verify the selected method updates and the exact satoshi amount is preserved, then close without paying. If switching methods changes the payment terms or finds them expired, verify iOS returns to Amount for review. Reopen the unpaid request and close without paying. + Create a USD request accepting Bitcoin and USDT whose quoted USDT amount is greater than the payer's USDT balance. On the payer, open the request and verify Bitcoin starts selected; USDT remains available for manual selection. Close without continuing. Repeat with a request whose quoted USDT amount is covered by that balance and verify USDT starts selected. This initial choice covers the requested amount only; fees are checked when preparing payment. When both Savings and Spending are accepted and existing regtest funds cover the payment and fees, continue with Bitcoin to confirmation and change between them. On Android, verify valid quoted Bitcoin terms keep confirmation open with the selected method, its exact quoted satoshi amount and refreshed fees; payment still requires a swipe. If the target terms are unavailable or expired, or the Lightning amount is below its minimum, verify Amount opens for review. After a valid switch, open Amount manually; tap its top-bar Back arrow and verify the sheet closes without returning to the previous confirmation. On iOS, unchanged valid payment terms keep confirmation open: verify the selected method updates and the exact satoshi amount is preserved, then close without paying. If switching methods changes the payment terms or finds them expired, verify iOS returns to Amount for review. Reopen the unpaid request and close without paying. Create a BTC request accepting Savings and USDT. If Spending is available, deselect it before sending. On the payer's Amount screen, verify Savings and USDT are available but Spending is not. Switch between the accepted sources and verify the displayed amount preserves the request's quoted value and cannot be edited. Close without continuing. - Create a USD subscription for 150,50 on a comma-decimal locale. Verify the amount review shows 150,50 USD and editing preserves that amount. Verify Accepted Payments appears below frequency, with USDT selected and any available Savings and Spending chips unselected. Verify the description and icon controls are side by side. Propose it to the linked contact. On the payer, verify the subscription card and details show 150,50 USD, then review its recurrence and USDT-only payment method. Verify the subscription swipe uses USDT teal, then close before subscribing. + Create a USD subscription for 150,50 on a comma-decimal locale. In its amount editor, verify the same single filled denomination control (id "PaymentRequestDenominationToggle") shows USD. Toggle to BTC and back, verifying the label follows the denomination and exactly 150,50 USD is restored. Verify the amount review shows 150,50 USD and editing preserves that amount. Verify Accepted Payments appears below frequency, with USDT selected and any available Savings and Spending chips unselected. Verify the description and icon controls are side by side. Propose it to the linked contact. On the payer, verify the subscription card and details show 150,50 USD, then review its recurrence and USDT-only payment method. Verify the subscription swipe uses USDT teal, then close before subscribing. Create a BTC subscription for 12,345 sats and verify available Savings and Spending chips start selected and USDT is unselected. Deselect each selected Bitcoin chip and verify Choose Recipient is disabled when none remain. Select Savings (id "SubscriptionAccept-savings"), then USDT (id "SubscriptionAccept-usdt"); verify the amount converts to USD and only USDT is selected. Select Savings again; verify the amount returns to exactly 12,345 sats and only Savings is selected. If Spending is available, select it (id "SubscriptionAccept-spending") and verify both Bitcoin rails can be selected together, with USDT still unselected. Repeat the subscription USDT and Savings switches four times without editing. Verify every return to Bitcoin shows exactly 12,345 sats and USDT is never selected with a Bitcoin rail. Edit the amount to 10 USD, repeat the switches and verify every return to USDT shows exactly 10 USD. Select Savings only and propose to the linked contact. On the payer, verify the proposal offers Savings but not Spending or USDT, then close before subscribing. Open a linked USDT-only contact that has published a private payment list. Verify the amount screen offers only USDT, enter 0.01, and verify the balance is labeled AVAILABLE rather than AVAILABLE (SPENDING). Compare with direct USDT Send: the amount, balance, USDT chip and Continue use the same typography; the balance stays close below the amount and the keypad uses the remaining space without clipping Continue. Close without continuing to review. diff --git a/journeys/subscriptions/create-and-propose.xml b/journeys/subscriptions/create-and-propose.xml index 33758fcf14..df3883ce96 100644 --- a/journeys/subscriptions/create-and-propose.xml +++ b/journeys/subscriptions/create-and-propose.xml @@ -13,7 +13,7 @@ Tap Create (id "SubscriptionCreate") Verify the create sheet (id "CreateSubscription") appears with Choose Recipient disabled With the keyboard closed, scroll the form and verify Choose Recipient (id "SubscriptionChooseRecipient") stays pinned while the fields scroll behind it. Scroll to the end and verify Description and Custom icon clear the button. Repeat with large text. Focus Name and Description and verify the focused field and Choose Recipient remain reachable above the keyboard, then dismiss the keyboard. - Tap the amount pencil (id "SubscriptionEditAmount") and verify USD is not offered while USDT is disabled + Tap the amount pencil (id "SubscriptionEditAmount") and verify the denomination control (id "PaymentRequestDenominationToggle") shows BTC without a swap icon. Tap it and verify it stays BTC; USD is not offered while USDT is disabled Tap the Bitcoin/fiat unit control (id "PaymentRequestAmountUnit") twice and verify both input units remain available for the BTC denomination. Select Bitcoin units, enter "5000" on the keypad, and tap Continue (id "PaymentRequestAmountContinue") Verify the amount reads 5,000 sats and the accepted methods show separate selected Savings and Spending chips for enabled rails, each with a trailing X, and no USDT. A removed method retains its asset color at 50% opacity with a trailing +; tap it to restore it. If both rails are available, remove one and verify the other remains selected. Remove all selected rails and verify Choose Recipient stays disabled; restore the available Bitcoin rails before continuing. Type "Journey Sub" into the subscription name field (id "SubscriptionName")