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
54 changes: 49 additions & 5 deletions Bitkit/Managers/ToastWindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,22 @@ class ToastWindowManager: ObservableObject {
}

func showToast(_ toast: Toast) {
// Ensure window is set up before showing toast
ensureWindowExists()

// If window still doesn't exist after trying to set it up, log and return
guard let window = toastWindow else {
Logger.error("ToastWindowManager: Cannot show toast - window not available")
return
}

// Dismiss any existing toast first
cancelAutoHide()
toastWindow?.hasToast = false
toastWindow?.toastFrame = .zero
window.hasToast = false
window.toastFrame = .zero

// Update window's toast state for hit testing
toastWindow?.hasToast = true
window.hasToast = true

// Show the toast with animation
withAnimation(.easeInOut(duration: 0.4)) {
Expand All @@ -68,7 +77,7 @@ class ToastWindowManager: ObservableObject {
}

func pauseAutoHide() {
guard autoHideStartTime != nil else { return } // Already paused or no auto-hide
guard autoHideStartTime != nil else { return } // No active auto-hide to pause
cancelAutoHide()

// Calculate remaining time
Expand Down Expand Up @@ -124,11 +133,32 @@ class ToastWindowManager: ObservableObject {
autoHideDuration = 0
}

private func ensureWindowExists() {
// Check if window already exists and is still valid
if let existingWindow = toastWindow,
existingWindow.windowScene != nil,
!existingWindow.isHidden
Comment on lines +139 to +140
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if the window is not hidden, but a valid window could be hidden and still functional. Consider removing the !existingWindow.isHidden check, as the window should be shown when needed regardless of its current hidden state.

Suggested change
existingWindow.windowScene != nil,
!existingWindow.isHidden
existingWindow.windowScene != nil

Copilot uses AI. Check for mistakes.
{
return
}

// Window doesn't exist or is invalid, try to set it up
setupToastWindow()
}

private func setupToastWindow() {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
// Try to find an active window scene
guard let windowScene = findActiveWindowScene() else {
Logger.warn("ToastWindowManager: No active window scene available")
return
}

// Clean up old window if it exists
if let oldWindow = toastWindow {
oldWindow.isHidden = true
oldWindow.rootViewController = nil
}

let window = PassThroughWindow(windowScene: windowScene)
window.windowLevel = UIWindow.Level.alert + 1 // Above alerts and sheets
window.backgroundColor = .clear
Expand All @@ -143,6 +173,20 @@ class ToastWindowManager: ObservableObject {
toastWindow = window
toastHostingController = hostingController
}

private func findActiveWindowScene() -> UIWindowScene? {
// Try to find an active window scene from connected scenes
for scene in UIApplication.shared.connectedScenes {
if let windowScene = scene as? UIWindowScene,
windowScene.activationState == .foregroundActive || windowScene.activationState == .foregroundInactive
Comment on lines +179 to +181
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activation state check could be simplified and made more maintainable by storing the valid states in an array. Consider: let validStates: [UIScene.ActivationState] = [.foregroundActive, .foregroundInactive] and then validStates.contains(windowScene.activationState)

Suggested change
for scene in UIApplication.shared.connectedScenes {
if let windowScene = scene as? UIWindowScene,
windowScene.activationState == .foregroundActive || windowScene.activationState == .foregroundInactive
let validStates: [UIScene.ActivationState] = [.foregroundActive, .foregroundInactive]
for scene in UIApplication.shared.connectedScenes {
if let windowScene = scene as? UIWindowScene,
validStates.contains(windowScene.activationState)

Copilot uses AI. Check for mistakes.
{
return windowScene
}
}

// Fallback to any window scene if no active one found
return UIApplication.shared.connectedScenes.first as? UIWindowScene
}
}

// Custom window that only intercepts touches on interactive elements
Expand Down
18 changes: 16 additions & 2 deletions Bitkit/ViewModels/AmountInputViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ class AmountInputViewModel: ObservableObject {
amountSats = newAmountSats
displayText = formatDisplayTextFromAmount(newAmountSats, currency: currency)
// Update raw input text based on the formatted display
// Remove formatting separators (spaces for modern Bitcoin, commas for fiat)
if currency.primaryDisplay == .fiat {
rawInputText = displayText.replacingOccurrences(of: ",", with: "")
} else if currency.displayUnit == .modern {
// Modern Bitcoin uses spaces as grouping separators
rawInputText = displayText.replacingOccurrences(of: " ", with: "")
} else {
rawInputText = displayText
}
Expand All @@ -115,8 +119,12 @@ class AmountInputViewModel: ObservableObject {
if amountSats > 0 {
displayText = formatDisplayTextFromAmount(amountSats, currency: currency)
// Update raw input text based on the formatted display
// Remove formatting separators (spaces for modern Bitcoin, commas for fiat)
if currency.primaryDisplay == .fiat {
rawInputText = displayText.replacingOccurrences(of: ",", with: "")
} else if currency.displayUnit == .modern {
// Modern Bitcoin uses spaces as grouping separators
rawInputText = displayText.replacingOccurrences(of: " ", with: "")
} else {
rawInputText = displayText
}
Expand All @@ -135,8 +143,14 @@ class AmountInputViewModel: ObservableObject {
// First convert fiat to sats, then format for Bitcoin display
let cleanFiat = currentRawInput.replacingOccurrences(of: ",", with: "")
if let fiatValue = Double(cleanFiat), let sats = currency.convert(fiatAmount: fiatValue) {
rawInputText = formatBitcoinFromSats(sats, isModern: currency.displayUnit == .modern)
displayText = rawInputText
let formatted = formatBitcoinFromSats(sats, isModern: currency.displayUnit == .modern)
displayText = formatted
// Remove spaces from rawInputText for modern Bitcoin
if currency.displayUnit == .modern {
rawInputText = formatted.replacingOccurrences(of: " ", with: "")
} else {
rawInputText = formatted
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions Bitkit/Views/Settings/DevSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ struct DevSettingsView: View {
SettingsListLabel(title: "Test Push Notification", rightIcon: nil)
}

Button {
fatalError("Simulate Crash")
} label: {
SettingsListLabel(title: "Simulate Crash", rightIcon: nil)
}

Button {
Task {
do {
Expand Down
12 changes: 10 additions & 2 deletions Bitkit/Views/Wallets/Send/SendAmountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ struct SendAmountView: View {
maxSendableAmount = nil
}
}
.onChange(of: wallet.selectedFeeRateSatsPerVByte) { _ in
// Recalculate max sendable amount when fee rate becomes available or changes
if app.selectedWalletToPayFrom == .onchain {
Task {
await calculateMaxSendableAmount()
}
}
}
Comment on lines +177 to +184
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onChange closure uses an unnamed parameter '_' but never checks if the fee rate is valid or non-zero. Consider adding a guard statement to verify the fee rate is set before recalculating: guard wallet.selectedFeeRateSatsPerVByte > 0 else { return }

Copilot uses AI. Check for mistakes.
}

private func onContinue() async {
Expand Down Expand Up @@ -258,8 +266,8 @@ struct SendAmountView: View {
} catch {
Logger.error("Failed to calculate max sendable amount: \(error)")
await MainActor.run {
// Fall back to total balance if calculation fails
maxSendableAmount = UInt64(wallet.spendableOnchainBalanceSats)
// Keep as nil on error - availableAmount will fall back to total balance
maxSendableAmount = nil
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions BitkitTests/NumberPadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,33 @@ final class NumberPadTests: XCTestCase {
XCTAssertEqual(viewModel.amountSats, 0)
}

func testDeleteAfterMaxAmountModernBitcoin() {
let viewModel = AmountInputViewModel()
let currency = mockCurrency(primaryDisplay: .bitcoin, displayUnit: .modern)

// Set max amount using updateFromSats (simulates tapping max button)
// This creates formatted text with spaces (e.g., "100 000 000")
viewModel.updateFromSats(100_000_000, currency: currency)
XCTAssertEqual(viewModel.displayText, "100 000 000")
XCTAssertEqual(viewModel.amountSats, 100_000_000)

// Delete should remove digits, not spaces
// After first delete, should be "10 000 000" (one digit removed)
viewModel.handleNumberPadInput("delete", currency: currency)
XCTAssertEqual(viewModel.displayText, "10 000 000")
XCTAssertEqual(viewModel.amountSats, 10_000_000)

// Delete again
viewModel.handleNumberPadInput("delete", currency: currency)
XCTAssertEqual(viewModel.displayText, "1 000 000")
XCTAssertEqual(viewModel.amountSats, 1_000_000)

// Delete again
viewModel.handleNumberPadInput("delete", currency: currency)
XCTAssertEqual(viewModel.displayText, "100 000")
XCTAssertEqual(viewModel.amountSats, 100_000)
}

// MARK: - Leading Zero Tests

func testLeadingZeroBehavior() {
Expand Down
Loading