Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1578.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions journeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
26 changes: 26 additions & 0 deletions journeys/send/retry-original-dialog-style.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<journey name="retry original dialog style">
<description>
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.
</description>
<actions>
<action>Tap Retry original payment</action>
<action>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</action>
<action>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</action>
<action>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</action>
<action>Clear the fee field and enter the original fee rate; verify the error disappears and Continue is enabled</action>
<action>Tap Continue</action>
<action>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</action>
<action>Tap Cancel and verify Payment Pending still shows "Retry original payment"</action>
</actions>
</journey>
Loading