From f2af5ecd646609148a18269230d6a04271109285 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 19 Oct 2026 11:14:46 -0300 Subject: [PATCH 1/2] fix: use app dialog style for retry original payment Co-Authored-By: Claude Opus 5.5 --- .../to/bitkit/ui/scaffold/AppAlertDialog.kt | 4 +- .../screens/wallets/send/SendPendingScreen.kt | 132 +++++++++--------- changelog.d/next/1544.fixed.md | 1 + journeys/README.md | 1 + journeys/send/retry-original-dialog-style.xml | 26 ++++ 5 files changed, 95 insertions(+), 69 deletions(-) create mode 100644 changelog.d/next/1544.fixed.md create mode 100644 journeys/send/retry-original-dialog-style.xml diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt index deece27b01..9f9eb53fd3 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt @@ -71,6 +71,7 @@ fun AppAlertDialog( dismissTextColor: Color = Colors.White64, confirmTestTag: String = "DialogConfirm", dismissTestTag: String = "DialogCancel", + confirmEnabled: Boolean = true, onDismissRequest: () -> Unit = onDismiss, properties: DialogProperties = DialogProperties( dismissOnClickOutside = false, @@ -84,9 +85,10 @@ fun AppAlertDialog( confirmButton = { TextButton( onClick = rememberDebouncedClick(onClick = onConfirm), + enabled = confirmEnabled, modifier = Modifier.testTag(confirmTestTag) ) { - BodyMSB(text = confirmText, color = confirmTextColor) + BodyMSB(text = confirmText, color = if (confirmEnabled) confirmTextColor else Colors.White32) } }, dismissButton = { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPendingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPendingScreen.kt index 7007b7fc27..c38681fab8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPendingScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPendingScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -36,6 +35,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.filterNotNull @@ -50,7 +50,10 @@ import to.bitkit.repositories.PendingPaymentResolution import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BiometricsView import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput @@ -247,34 +250,29 @@ private fun RecoveryAuthorization( if (viewModel.openRecoveryPin()) onNavigateToPin() } when (val approval = uiState.recoveryApproval) { - is RecoveryApproval.Fee -> AlertDialog( - onDismissRequest = { viewModel.answerRecoveryApproval(false) }, - title = { BodyM(stringResource(R.string.wallet__send_pending__retry_title)) }, - text = { - Column { - SelectionContainer { BodyM(approval.receipt.address, color = Colors.White64) } - BalanceHeaderView(sats = approval.receipt.amountSats.toLong()) - VerticalSpacer(16.dp) - BodyM( - stringResource(R.string.wallet__send_fee_total) - .replace("{feeSats}", requireNotNull(approval.receipt.miningFeeSats).toString()) - ) - BodyM("${approval.receipt.feeRateSatsPerVByte} " + stringResource(R.string.common__sat_vbyte)) - } - }, - confirmButton = { - PrimaryButton( - text = stringResource(R.string.wallet__send_pending__retry_authorize), - onClick = { viewModel.answerRecoveryApproval(true) } + is RecoveryApproval.Fee -> AppAlertDialog( + title = stringResource(R.string.wallet__send_pending__retry_title), + confirmText = stringResource(R.string.wallet__send_pending__retry_authorize), + dismissText = stringResource(R.string.common__cancel), + onConfirm = { viewModel.answerRecoveryApproval(true) }, + onDismiss = { viewModel.answerRecoveryApproval(false) }, + properties = DialogProperties(), + ) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + RecoveryRecipient(approval.receipt.address, approval.receipt.amountSats.toLong()) + VerticalSpacer(16.dp) + Caption13Up(stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) + VerticalSpacer(8.dp) + BodySSB( + stringResource(R.string.wallet__send_fee_total) + .replace("{feeSats}", requireNotNull(approval.receipt.miningFeeSats).toString()) ) - }, - dismissButton = { - SecondaryButton( - text = stringResource(R.string.common__cancel), - onClick = { viewModel.answerRecoveryApproval(false) } + BodyM( + "${approval.receipt.feeRateSatsPerVByte} " + stringResource(R.string.common__sat_vbyte), + color = Colors.White64, ) - }, - ) + } + } is RecoveryApproval.Release -> AppAlertDialog( title = stringResource(R.string.wallet__send_pending__abandon), text = stringResource(R.string.wallet__send_pending__abandon_release) @@ -327,49 +325,47 @@ private fun RecoveryFeeDialog( onClose: () -> Unit, onAuthorize: (ULong) -> Unit, ) { - AlertDialog( - onDismissRequest = onClose, - title = { BodyM(stringResource(R.string.wallet__send_pending__retry_title)) }, - text = { - Column { - BodyM(stringResource(R.string.wallet__send_pending__retry_description)) - original.let { original -> - VerticalSpacer(16.dp) - SelectionContainer { BodyM(original.address, color = Colors.White64) } - BalanceHeaderView(sats = original.amountSats.toLong()) - } - VerticalSpacer(16.dp) - TextInput( - value = feeInput, - onValueChange = onFeeChange, - placeholder = stringResource(R.string.common__sat_vbyte), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - isError = feeInput.isNotBlank() && OnchainRecoveryFeeRate.parse(feeInput) == null, - supportingText = { - if (feeInput.isNotBlank() && OnchainRecoveryFeeRate.parse(feeInput) == null) { - BodyM(stringResource(R.string.wallet__send_pending__retry_invalid_fee)) - } - }, - ) - } - }, - confirmButton = { - PrimaryButton( - text = stringResource(R.string.common__continue), - enabled = OnchainRecoveryFeeRate.parse(feeInput) != null, - onClick = { - val rate = OnchainRecoveryFeeRate.parse(feeInput) ?: return@PrimaryButton - onAuthorize(rate) + val feeRate = OnchainRecoveryFeeRate.parse(feeInput) + val isInvalid = feeInput.isNotBlank() && feeRate == null + AppAlertDialog( + title = stringResource(R.string.wallet__send_pending__retry_title), + confirmText = stringResource(R.string.common__continue), + dismissText = stringResource(R.string.common__cancel), + confirmEnabled = feeRate != null, + onConfirm = { feeRate?.let(onAuthorize) }, + onDismiss = onClose, + properties = DialogProperties(), + ) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + BodyM(stringResource(R.string.wallet__send_pending__retry_description), color = Colors.White64) + VerticalSpacer(16.dp) + RecoveryRecipient(original.address, original.amountSats.toLong()) + VerticalSpacer(16.dp) + Caption13Up(stringResource(R.string.wallet__send_fee_custom), color = Colors.White64) + VerticalSpacer(8.dp) + TextInput( + value = feeInput, + onValueChange = onFeeChange, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + suffix = { BodySSB(stringResource(R.string.common__sat_vbyte), color = Colors.White64) }, + isError = isInvalid, + supportingText = { + if (isInvalid) BodyS(stringResource(R.string.wallet__send_pending__retry_invalid_fee)) }, + modifier = Modifier.fillMaxWidth() ) - }, - dismissButton = { - SecondaryButton( - text = stringResource(R.string.common__cancel), - onClick = onClose, - ) - }, - ) + } + } +} + +@Composable +private fun RecoveryRecipient(address: String, amountSats: Long) { + Caption13Up(stringResource(R.string.wallet__send_to), color = Colors.White64) + VerticalSpacer(8.dp) + SelectionContainer { BodySSB(address) } + VerticalSpacer(16.dp) + BalanceHeaderView(sats = amountSats, modifier = Modifier.fillMaxWidth()) } @Composable diff --git a/changelog.d/next/1544.fixed.md b/changelog.d/next/1544.fixed.md new file mode 100644 index 0000000000..f8df888d05 --- /dev/null +++ b/changelog.d/next/1544.fixed.md @@ -0,0 +1 @@ +Retry original payment now uses Bitkit's dialog style, with a labelled recipient, the usual amount block and a fee field showing its unit. diff --git a/journeys/README.md b/journeys/README.md index c3b8138fc1..059e46249e 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -167,6 +167,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `restore-wallet/paste-seed-fragment.xml` | not ported — the iOS Restore screen still has the 12/24-only paste guard, so the behaviour does not exist there yet | | `send/own-invoice-guard.xml` | not ported — iOS has no own-invoice guard | | `send/recovery-pin-system-back.xml` | not ported — iOS has no system Back button, and `SendPinScreen` already answers cancel in `onDisappear` | +| `send/retry-original-dialog-style.xml` | not ported — iOS shows the two Retry original payment steps as native alerts | | `send/prepared-fee-recovery.md` | Same four manual fault cases and file on both platforms; the recipe records original-send versus replacement fee retention and iOS Max/identity-flow adaptations. These faults are not journey capabilities. | | `settings/electrum-server-error-toasts.xml` | not ported — iOS still shows one generic message for every manual Electrum connect failure | | `transfers/closed-channel-transfer-settles.xml` | not ported — the closed-channel and order-closure settle rules are an iOS follow-up | diff --git a/journeys/send/retry-original-dialog-style.xml b/journeys/send/retry-original-dialog-style.xml new file mode 100644 index 0000000000..86e04b67cf --- /dev/null +++ b/journeys/send/retry-original-dialog-style.xml @@ -0,0 +1,26 @@ + + + Both steps of Retry original payment on Payment Pending use the app's dialog: a title in the + Title style on the Gray5 surface, text-style Cancel and confirm buttons, the recipient under a + "To" label, the amount block as on Send review with its fiat line above the sats, and a fee field + labelled "Set Custom Fee" with the ₿ / vbyte unit inside the field. + + Precondition: an onboarded funded dev wallet with Payment Pending showing "Retry original + payment" for an on-chain send whose broadcast timed out. To get there, start an on-chain send to + any regtest address and run `adb shell cmd connectivity airplane-mode enable` right after the + swipe; turn airplane mode off again once Payment Pending shows the retry button. PIN for + payments may be on or off; the journey stops before authorizing. + + Android only: iOS shows the same two steps as native alerts. + + + Tap Retry original payment + Verify from a screenshot that the dialog's title reads "Retry original payment" in the Title style, the description sits below it, "TO" labels the recipient address, and the amount shows the fiat line above the sats with space between it and the address + Verify the fee field is labelled "SET CUSTOM FEE", is prefilled with the original fee rate, and shows "₿ / vbyte" inside the field at its end + Clear the fee field and enter 1000; verify the error "Enter a valid positive fee rate in sat/vB." appears under the field and Continue is greyed out and does nothing when tapped + Clear the fee field and enter the original fee rate; verify the error disappears and Continue is enabled + Tap Continue + Verify from a screenshot that the approval step shows the same title, "TO" with the recipient, the amount block, "SPEED AND FEE" with the mining fee in sats and the fee rate below it, and the buttons Cancel and Authorize retry + Tap Cancel and verify Payment Pending still shows "Retry original payment" + + From 5df425c2b5f9ba5798d38a0754d499dcfec40d87 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 19 Oct 2026 12:30:50 -0300 Subject: [PATCH 2/2] chore: rename changelog fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/next/{1544.fixed.md => 1578.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1544.fixed.md => 1578.fixed.md} (100%) diff --git a/changelog.d/next/1544.fixed.md b/changelog.d/next/1578.fixed.md similarity index 100% rename from changelog.d/next/1544.fixed.md rename to changelog.d/next/1578.fixed.md