Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions Bitkit/Components/ActivityIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct ActivityIndicator: View {
let size: CGFloat
let theme: Theme
let tint: Color?

enum Theme {
case light
Expand All @@ -12,13 +13,14 @@ struct ActivityIndicator: View {
@State private var isRotating = false
@State private var opacity: Double = 0

init(size: CGFloat = 32, theme: Theme = .light) {
init(size: CGFloat = 32, theme: Theme = .light, tint: Color? = nil) {
self.size = size
self.theme = theme
self.tint = tint
}

var body: some View {
let color = theme == .light ? Color.white : Color.black
let color = tint ?? (theme == .light ? Color.white : Color.black)

ZStack {
Circle()
Expand Down
10 changes: 8 additions & 2 deletions Bitkit/Components/NumberPadActionButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct NumberPadActionButton: View {
var color: Color = .purpleAccent
var variant: NumberPadActionButtonVariant = .primary
var disabled: Bool = false
var isLoading: Bool = false
var action: () -> Void

@State private var isPressed = false
Expand All @@ -21,7 +22,10 @@ struct NumberPadActionButton: View {
action()
} label: {
HStack(spacing: 8) {
if let imageName {
if isLoading {
ActivityIndicator(size: 10, tint: color)
.frame(width: 16, height: 16)
} else if let imageName {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
Expand All @@ -40,7 +44,7 @@ struct NumberPadActionButton: View {
)
.cornerRadius(8)
}
.disabled(disabled)
.disabled(disabled || isLoading)
.buttonStyle(NoAnimationButtonStyle())
.pressEvents(
onPress: {
Expand All @@ -50,6 +54,8 @@ struct NumberPadActionButton: View {
isPressed = false
}
)
.animation(.easeInOut(duration: 0.2), value: isLoading)
.animation(.easeInOut(duration: 0.2), value: text)
}

private var background: some View {
Expand Down
14 changes: 11 additions & 3 deletions Bitkit/Components/SwipeButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import SwiftUI
struct SwipeButton: View {
let title: String
let accentColor: Color
/// Blocks the swipe and shows the knob spinner while a prerequisite is still loading.
/// Blocks interaction without presenting the post-swipe loading state.
var isDisabled = false
/// Blocks interaction and shows the knob spinner while an operation is running.
var isLoading = false
/// Optional binding for swipe progress (0...1), e.g. to drive animations in the parent.
var swipeProgress: Binding<CGFloat>?
Expand All @@ -13,6 +15,10 @@ struct SwipeButton: View {
@State private var isSubmitting = false

private var isBusy: Bool {
isDisabled || isLoading || isSubmitting
}

private var showsSpinner: Bool {
isLoading || isSubmitting
}

Expand Down Expand Up @@ -43,19 +49,20 @@ struct SwipeButton: View {
.frame(height: buttonHeight - innerPadding)
.padding(.horizontal, innerPadding / 2)
}
.opacity(isDisabled ? 0.5 : 1)

// Track text
BodySSBText(title)
.frame(maxWidth: .infinity, alignment: .center)
.opacity(Double(1.0 - textProgress))
.opacity(Double(1.0 - textProgress) * (isDisabled ? 0.5 : 1))

// Knob
Circle()
.fill(accentColor)
.frame(width: buttonHeight - innerPadding, height: buttonHeight - innerPadding)
.overlay(
ZStack {
if isBusy {
if showsSpinner {
ActivityIndicator(theme: .dark)
} else {
Image("arrow-right")
Expand All @@ -75,6 +82,7 @@ struct SwipeButton: View {
.accessibilityIdentifier("GRAB")
.offset(x: clampedOffset)
.padding(.horizontal, innerPadding / 2)
.opacity(isDisabled ? 0.5 : 1)
.gesture(
DragGesture()
.onChanged { value in
Expand Down
42 changes: 32 additions & 10 deletions Bitkit/ViewModels/HwFundingSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ final class HwSendCoordinator {

private(set) var availableSats: UInt64 = 0
private(set) var previewFeeSats: UInt64 = 0
private(set) var isFundingSourceLoading = false
private(set) var isPreviewLoading = false
private(set) var isSigning = false
private(set) var isBroadcastUnresolved = false
private(set) var isPassphraseRequired = false
Expand Down Expand Up @@ -346,7 +348,11 @@ final class HwSendCoordinator {
self.availableSats = availableSats
}

func selectWallet(_ walletId: String?, initialAvailableSats: UInt64 = 0) {
func selectWallet(
_ walletId: String?,
initialAvailableSats: UInt64 = 0,
showsLoading: Bool = false
) {
guard self.walletId != walletId else { return }
guard operationTask == nil, !isBroadcastUnresolved else { return }

Expand All @@ -356,6 +362,8 @@ final class HwSendCoordinator {
pendingPayment = nil
availableSats = walletId == nil ? 0 : initialAvailableSats
previewFeeSats = 0
isFundingSourceLoading = walletId != nil && showsLoading
isPreviewLoading = false
isSigning = false
isBroadcastUnresolved = false
isPassphraseRequired = false
Expand All @@ -372,6 +380,7 @@ final class HwSendCoordinator {
guard !destinationAddress.isEmpty else {
if self.walletId == walletId {
availableSats = manager.fundingBalance(walletId: walletId)
isFundingSourceLoading = false
}
return
}
Expand All @@ -382,6 +391,7 @@ final class HwSendCoordinator {
func apply(_ available: UInt64) {
guard self.walletId == walletId, availabilityRequestId == requestId else { return }
availableSats = available
isFundingSourceLoading = false
}

do {
Expand All @@ -407,19 +417,27 @@ final class HwSendCoordinator {
guard let walletId else { return nil }
previewRequestId += 1
let requestId = previewRequestId
isPreviewLoading = true
previewFeeSats = 0
let request = PaymentRequest(address: address, sats: sats, satsPerVByte: satsPerVByte)
if pendingPayment?.request != request {
pendingPayment = nil
}
let fee = try await manager.estimateOfflineFundingMiningFee(
walletId: walletId,
address: address,
sats: sats,
satsPerVByte: satsPerVByte
)
guard self.walletId == walletId, previewRequestId == requestId else { return nil }
previewFeeSats = fee
return fee
do {
let signer = signerFactory(manager, address, satsPerVByte)
let fee = try await signer.estimateOfflineFundingMiningFee(walletId: walletId, address: address, sats: sats)
guard self.walletId == walletId, previewRequestId == requestId else { return nil }
previewFeeSats = fee
isFundingSourceLoading = false
isPreviewLoading = false
return fee
} catch {
if self.walletId == walletId, previewRequestId == requestId {
isFundingSourceLoading = false
isPreviewLoading = false
}
throw error
}
}

func signAndBroadcast(
Expand Down Expand Up @@ -509,6 +527,10 @@ final class HwSendCoordinator {
}

func cancel() {
availabilityRequestId += 1
previewRequestId += 1
isFundingSourceLoading = false
isPreviewLoading = false
isVerifyingPassphrase = false
isPassphraseRequired = false
guard !isBroadcastUnresolved else { return }
Expand Down
8 changes: 5 additions & 3 deletions Bitkit/Views/Wallets/Send/SendAmountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ struct SendAmountView: View {
imageName: canSwitchFundingSource ? "arrow-up-down" : nil,
color: selectedSourceColor,
variant: canSwitchFundingSource ? .primary : .secondary,
disabled: !canSwitchFundingSource || isContinuing
disabled: !canSwitchFundingSource || isContinuing,
isLoading: hwSend.isFundingSourceLoading
) {
selectNextFundingSource()
}
Expand Down Expand Up @@ -183,7 +184,7 @@ struct SendAmountView: View {

CustomButton(
title: t("common__continue"),
isDisabled: !isValidAmount,
isDisabled: !isValidAmount || hwSend.isFundingSourceLoading,
isLoading: isContinuing
) {
await onContinue()
Expand Down Expand Up @@ -368,7 +369,8 @@ struct SendAmountView: View {
)
hwSend.selectWallet(
walletId,
initialAvailableSats: balance > reserve ? balance - reserve : 0
initialAvailableSats: balance > reserve ? balance - reserve : 0,
showsLoading: true
Comment thread
ben-kaufman marked this conversation as resolved.
)
app.selectedWalletToPayFrom = .onchain
}
Expand Down
69 changes: 50 additions & 19 deletions Bitkit/Views/Wallets/Send/SendConfirmationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ struct SendConfirmationView: View {
?? t("hardware__device_model_trezor")
}

private var isHardwarePreparationLoading: Bool {
hwSend.isActive && (hwSend.isFundingSourceLoading || hwSend.isPreviewLoading)
}

private var isHardwareConfirmationUnavailable: Bool {
hwSend.isActive && (isHardwarePreparationLoading || hwSend.previewFeeSats == 0)
}

private var displayedTransactionFee: Int {
transactionFee > 0 ? transactionFee : Int(hwSend.previewFeeSats)
Comment thread
ben-kaufman marked this conversation as resolved.
}

/// `.instant` is only valid when paying from Lightning; align `selectedSpeed` with the current sat/vB on savings.
private func reconcileInstantSpeedWhenSwitchingToOnChain() async {
guard wallet.selectedSpeed == .instant else { return }
Expand Down Expand Up @@ -202,7 +214,12 @@ struct SendConfirmationView: View {
.accessibilityIdentifier("SendConfirmToggleDetails")
}

SwipeButton(title: t("wallet__send_swipe"), accentColor: accentColor, swipeProgress: $swipeProgress) {
SwipeButton(
title: t("wallet__send_swipe"),
accentColor: accentColor,
isDisabled: isHardwareConfirmationUnavailable,
swipeProgress: $swipeProgress
) {
try await submitPayment()
}
}
Expand Down Expand Up @@ -272,7 +289,8 @@ struct SendConfirmationView: View {
imageName: canSwitchFundingSource ? "arrow-up-down" : nil,
color: hwSend.isActive ? .blueAccent : .brandAccent,
variant: canSwitchFundingSource ? .primary : .secondary,
disabled: !canSwitchFundingSource
disabled: !canSwitchFundingSource || isHardwarePreparationLoading,
isLoading: hwSend.isFundingSourceLoading
) {
selectNextFundingSource()
}
Expand Down Expand Up @@ -307,29 +325,41 @@ struct SendConfirmationView: View {
}) {
SendSectionView(t("wallet__send_fee_and_speed")) {
HStack(spacing: 0) {
Image(wallet.selectedSpeed.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(wallet.selectedSpeed.iconColor)
.frame(width: 16, height: 16)
.padding(.trailing, 4)
Group {
if hwSend.isPreviewLoading {
ActivityIndicator(size: 10, tint: wallet.selectedSpeed.iconColor)
} else {
Image(wallet.selectedSpeed.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(wallet.selectedSpeed.iconColor)
}
}
.frame(width: 16, height: 16)
.padding(.trailing, 4)

if transactionFee > 0 {
let feeText = "\(wallet.selectedSpeed.title) ("
HStack(spacing: 0) {
BodySSBText(feeText)
MoneyText(sats: transactionFee, size: .bodySSB, symbol: true, symbolColor: .textPrimary)
HStack(spacing: 0) {
BodySSBText(wallet.selectedSpeed.title)
if displayedTransactionFee > 0 {
BodySSBText(" (")
MoneyText(
sats: displayedTransactionFee,
size: .bodySSB,
symbol: true,
symbolColor: .textPrimary
)
BodySSBText(")")
}

Image("pencil")
.foregroundColor(.textPrimary)
.frame(width: 12, height: 12)
.padding(.leading, 6)
}

Image("pencil")
.foregroundColor(.textPrimary)
.frame(width: 12, height: 12)
.padding(.leading, 6)
}
}
}
.disabled(isHardwarePreparationLoading)

SendSectionView(t("wallet__send_confirming_in")) {
HStack(spacing: 0) {
Expand Down Expand Up @@ -504,7 +534,8 @@ struct SendConfirmationView: View {
)
hwSend.selectWallet(
walletId,
initialAvailableSats: balance > reserve ? balance - reserve : 0
initialAvailableSats: balance > reserve ? balance - reserve : 0,
showsLoading: true
Comment thread
ben-kaufman marked this conversation as resolved.
)
app.selectedWalletToPayFrom = .onchain
}
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Wallets/Send/SendFeeCustom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ struct SendFeeCustom: View {
do {
try await wallet.setFeeRate(speed: .custom(satsPerVByte: feeRate))
app.selectedWalletToPayFrom = .onchain
await refreshHardwareMaxIfNeeded()
navigationPath.removeLast()
await refreshHardwareMaxIfNeeded()
} catch {
Logger.error("Failed to set custom fee rate: \(error)")
app.toast(
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Wallets/Send/SendFeeRate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ struct SendFeeRate: View {
} else {
try await wallet.setFeeRate(speed: speed)
app.selectedWalletToPayFrom = .onchain
await refreshHardwareMaxIfNeeded()
navigationPath.removeLast()
await refreshHardwareMaxIfNeeded()
}
} catch {
Logger.error("Error setting fee rate: \(error)", context: "SendFeeRate")
Expand Down
Loading
Loading