Compare commits

...

2 Commits

Author SHA1 Message Date
Nick Klockenga
561e3b4715
swiftformat correction 2026-04-02 22:33:12 -04:00
Nick Klockenga
f55bb0bacf
minor ui cleanup on the recipients send screen 2026-04-02 22:22:36 -04:00
3 changed files with 68 additions and 43 deletions

View File

@ -27,6 +27,7 @@ final class SendViewModel: PSBTFlowManaging {
var selectedFeePreset: FeePreset = .medium
var showAddressScanner: Bool = false
var scanTargetRecipientIndex: Int = 0
var focusAmountIndex: Int?
var recommendedFees: BitcoinService.RecommendedFees?
// UTXO selection
@ -134,6 +135,10 @@ final class SendViewModel: PSBTFlowManaging {
|| manualUTXOSelection
}
var isReviewReady: Bool {
!recipients.isEmpty && recipients.allSatisfy { !$0.isAddressEmpty && (!$0.isAmountEmpty || $0.isSendMax) }
}
var hasValidRecipients: Bool {
!recipients.isEmpty && recipients.allSatisfy { r in
r.isValidAddress && (r.isValidAmount || (r.isSendMax && r.amountValue != nil))

View File

@ -41,13 +41,6 @@ struct SendRecipientsView: View {
RecipientCard(viewModel: viewModel, index: index)
}
Button(action: { viewModel.addRecipient() }) {
Label("Add Recipient", systemImage: "plus.circle")
.font(.hbBody(15))
.foregroundStyle(viewModel.canAddRecipient ? Color.hbBitcoinOrange : Color.hbTextSecondary)
}
.disabled(!viewModel.canAddRecipient)
// Fee preset picker
FeePresetCard(viewModel: viewModel)
.padding(.horizontal, 24)
@ -67,13 +60,13 @@ struct SendRecipientsView: View {
if viewModel.isProcessing {
ProgressView()
.tint(.white)
.hbPrimaryButton()
.hbPrimaryButton(isEnabled: viewModel.isReviewReady)
} else {
Text("Review")
.hbPrimaryButton()
.hbPrimaryButton(isEnabled: viewModel.isReviewReady)
}
}
.disabled(viewModel.isProcessing)
.disabled(viewModel.isProcessing || !viewModel.isReviewReady)
.padding(.horizontal, 24)
if viewModel.hasAnyInput {
@ -94,8 +87,10 @@ struct SendRecipientsView: View {
}
.sheet(isPresented: $viewModel.showAddressScanner) {
AddressScannerSheet { scanned in
viewModel.parseBIP21(scanned, forRecipientAt: viewModel.scanTargetRecipientIndex)
let targetIndex = viewModel.scanTargetRecipientIndex
viewModel.parseBIP21(scanned, forRecipientAt: targetIndex)
viewModel.showAddressScanner = false
viewModel.focusAmountIndex = targetIndex
}
}
.sheet(isPresented: $viewModel.showUTXOPicker) {
@ -113,6 +108,7 @@ private struct RecipientCard: View {
@Bindable var viewModel: SendViewModel
let index: Int
@AppStorage(Constants.fiatEnabledKey) private var fiatEnabled = false
@FocusState private var isAmountFocused: Bool
private var recipient: Recipient? {
guard index < viewModel.recipients.count else { return nil }
@ -146,8 +142,8 @@ private struct RecipientCard: View {
private var cardContent: some View {
VStack(spacing: 12) {
// Header with remove button and action buttons
HStack {
// Header with paste and remove buttons
HStack(spacing: 0) {
Text("Recipient \(index + 1)")
.font(.hbLabel())
.foregroundStyle(Color.hbTextSecondary)
@ -163,16 +159,11 @@ private struct RecipientCard: View {
.font(.hbBody(13))
.foregroundStyle(Color.hbSteelBlue)
}
Button(action: {
viewModel.scanTargetRecipientIndex = index
viewModel.showAddressScanner = true
}) {
Label("Scan", systemImage: "qrcode.viewfinder")
.font(.hbBody(13))
.foregroundStyle(Color.hbBitcoinOrange)
}
}
// Trailing padding to align Paste right edge with address TextField right edge
// Scan button (44) + HStack spacing (8) = 52
.padding(.trailing, 52)
.overlay(alignment: .topTrailing) {
if viewModel.recipients.count > 1 {
Button(action: { viewModel.removeRecipient(at: index) }) {
Image(systemName: "xmark.circle.fill")
@ -182,24 +173,39 @@ private struct RecipientCard: View {
}
}
// Address
// Address + Scan
VStack(alignment: .leading, spacing: 6) {
TextField("bc1q... or tb1q...", text: $viewModel.recipients[index].address)
.font(.hbMono(13))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.padding(12)
.background(Color.hbSurfaceElevated)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(
(addressHasError || addressFormatError)
? Color.hbError.opacity(0.8) : .clear,
lineWidth: 1.5
)
)
.foregroundStyle(Color.hbTextPrimary)
HStack(spacing: 8) {
TextField("\(viewModel.currentNetwork?.addressPrefix ?? "bc1")q...", text: $viewModel.recipients[index].address)
.font(.hbMono(13))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.padding(12)
.background(Color.hbSurfaceElevated)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(
(addressHasError || addressFormatError)
? Color.hbError.opacity(0.8) : .clear,
lineWidth: 1.5
)
)
.foregroundStyle(Color.hbTextPrimary)
Button(action: {
viewModel.scanTargetRecipientIndex = index
viewModel.showAddressScanner = true
}) {
Image(systemName: "qrcode.viewfinder")
.font(.system(size: 20))
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(Color.hbBitcoinOrange)
.clipShape(RoundedRectangle(cornerRadius: 8))
.contentShape(Rectangle())
}
}
if addressFormatError {
let expected = viewModel.currentNetwork?.addressPrefix ?? "bc1/tb1"
@ -235,6 +241,7 @@ private struct RecipientCard: View {
TextField("0.00", text: fiatBinding(for: index))
.font(.hbMono(16))
.keyboardType(.decimalPad)
.focused($isAmountFocused)
.disabled(isMaxActive)
.foregroundStyle(isMaxActive ? Color.hbTextSecondary : Color.hbTextPrimary)
}
@ -252,6 +259,7 @@ private struct RecipientCard: View {
TextField("0", text: $viewModel.recipients[index].amountSats)
.font(.hbMono(16))
.keyboardType(.numberPad)
.focused($isAmountFocused)
.disabled(isMaxActive)
.padding(12)
.background(isMaxActive ? Color.hbSurfaceElevated.opacity(0.5) : Color.hbSurfaceElevated)
@ -300,6 +308,12 @@ private struct RecipientCard: View {
}
.hbCard()
.padding(.horizontal, 24)
.onChange(of: viewModel.focusAmountIndex) {
if viewModel.focusAmountIndex == index {
isAmountFocused = true
viewModel.focusAmountIndex = nil
}
}
}
@ViewBuilder

View File

@ -27,6 +27,10 @@ struct SendFlowView: View {
if viewModel.currentStep == .recipients {
Menu {
Button(action: { viewModel.addRecipient() }) {
Label("Add Recipient", systemImage: "plus")
}
.disabled(!viewModel.canAddRecipient)
Button(action: { viewModel.showLoadPSBT = true }) {
Label("Saved PSBTs", systemImage: "tray.and.arrow.down")
}
@ -37,9 +41,11 @@ struct SendFlowView: View {
Label("Import PSBT via File", systemImage: "doc")
}
} label: {
Image(systemName: "ellipsis.circle")
.font(.system(size: 18))
.foregroundStyle(Color.hbBitcoinOrange)
Image(systemName: "ellipsis")
.font(.system(size: 20))
.foregroundStyle(Color.hbTextSecondary)
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
}
}