Skip to content

Commit 6b2c8a7

Browse files
authored
Merge pull request #289 from synonymdev/fix/various-fixes
fix: various fixes
2 parents cfe52f2 + 0be7feb commit 6b2c8a7

File tree

5 files changed

+108
-9
lines changed

5 files changed

+108
-9
lines changed

Bitkit/Managers/ToastWindowManager.swift

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,22 @@ class ToastWindowManager: ObservableObject {
3535
}
3636

3737
func showToast(_ toast: Toast) {
38+
// Ensure window is set up before showing toast
39+
ensureWindowExists()
40+
41+
// If window still doesn't exist after trying to set it up, log and return
42+
guard let window = toastWindow else {
43+
Logger.error("ToastWindowManager: Cannot show toast - window not available")
44+
return
45+
}
46+
3847
// Dismiss any existing toast first
3948
cancelAutoHide()
40-
toastWindow?.hasToast = false
41-
toastWindow?.toastFrame = .zero
49+
window.hasToast = false
50+
window.toastFrame = .zero
4251

4352
// Update window's toast state for hit testing
44-
toastWindow?.hasToast = true
53+
window.hasToast = true
4554

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

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

7483
// Calculate remaining time
@@ -124,11 +133,32 @@ class ToastWindowManager: ObservableObject {
124133
autoHideDuration = 0
125134
}
126135

136+
private func ensureWindowExists() {
137+
// Check if window already exists and is still valid
138+
if let existingWindow = toastWindow,
139+
existingWindow.windowScene != nil,
140+
!existingWindow.isHidden
141+
{
142+
return
143+
}
144+
145+
// Window doesn't exist or is invalid, try to set it up
146+
setupToastWindow()
147+
}
148+
127149
private func setupToastWindow() {
128-
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
150+
// Try to find an active window scene
151+
guard let windowScene = findActiveWindowScene() else {
152+
Logger.warn("ToastWindowManager: No active window scene available")
129153
return
130154
}
131155

156+
// Clean up old window if it exists
157+
if let oldWindow = toastWindow {
158+
oldWindow.isHidden = true
159+
oldWindow.rootViewController = nil
160+
}
161+
132162
let window = PassThroughWindow(windowScene: windowScene)
133163
window.windowLevel = UIWindow.Level.alert + 1 // Above alerts and sheets
134164
window.backgroundColor = .clear
@@ -143,6 +173,20 @@ class ToastWindowManager: ObservableObject {
143173
toastWindow = window
144174
toastHostingController = hostingController
145175
}
176+
177+
private func findActiveWindowScene() -> UIWindowScene? {
178+
// Try to find an active window scene from connected scenes
179+
for scene in UIApplication.shared.connectedScenes {
180+
if let windowScene = scene as? UIWindowScene,
181+
windowScene.activationState == .foregroundActive || windowScene.activationState == .foregroundInactive
182+
{
183+
return windowScene
184+
}
185+
}
186+
187+
// Fallback to any window scene if no active one found
188+
return UIApplication.shared.connectedScenes.first as? UIWindowScene
189+
}
146190
}
147191

148192
// Custom window that only intercepts touches on interactive elements

Bitkit/ViewModels/AmountInputViewModel.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,12 @@ class AmountInputViewModel: ObservableObject {
9696
amountSats = newAmountSats
9797
displayText = formatDisplayTextFromAmount(newAmountSats, currency: currency)
9898
// Update raw input text based on the formatted display
99+
// Remove formatting separators (spaces for modern Bitcoin, commas for fiat)
99100
if currency.primaryDisplay == .fiat {
100101
rawInputText = displayText.replacingOccurrences(of: ",", with: "")
102+
} else if currency.displayUnit == .modern {
103+
// Modern Bitcoin uses spaces as grouping separators
104+
rawInputText = displayText.replacingOccurrences(of: " ", with: "")
101105
} else {
102106
rawInputText = displayText
103107
}
@@ -115,8 +119,12 @@ class AmountInputViewModel: ObservableObject {
115119
if amountSats > 0 {
116120
displayText = formatDisplayTextFromAmount(amountSats, currency: currency)
117121
// Update raw input text based on the formatted display
122+
// Remove formatting separators (spaces for modern Bitcoin, commas for fiat)
118123
if currency.primaryDisplay == .fiat {
119124
rawInputText = displayText.replacingOccurrences(of: ",", with: "")
125+
} else if currency.displayUnit == .modern {
126+
// Modern Bitcoin uses spaces as grouping separators
127+
rawInputText = displayText.replacingOccurrences(of: " ", with: "")
120128
} else {
121129
rawInputText = displayText
122130
}
@@ -135,8 +143,14 @@ class AmountInputViewModel: ObservableObject {
135143
// First convert fiat to sats, then format for Bitcoin display
136144
let cleanFiat = currentRawInput.replacingOccurrences(of: ",", with: "")
137145
if let fiatValue = Double(cleanFiat), let sats = currency.convert(fiatAmount: fiatValue) {
138-
rawInputText = formatBitcoinFromSats(sats, isModern: currency.displayUnit == .modern)
139-
displayText = rawInputText
146+
let formatted = formatBitcoinFromSats(sats, isModern: currency.displayUnit == .modern)
147+
displayText = formatted
148+
// Remove spaces from rawInputText for modern Bitcoin
149+
if currency.displayUnit == .modern {
150+
rawInputText = formatted.replacingOccurrences(of: " ", with: "")
151+
} else {
152+
rawInputText = formatted
153+
}
140154
}
141155
}
142156
}

Bitkit/Views/Settings/DevSettingsView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ struct DevSettingsView: View {
102102
SettingsListLabel(title: "Test Push Notification", rightIcon: nil)
103103
}
104104

105+
Button {
106+
fatalError("Simulate Crash")
107+
} label: {
108+
SettingsListLabel(title: "Simulate Crash", rightIcon: nil)
109+
}
110+
105111
Button {
106112
Task {
107113
do {

Bitkit/Views/Wallets/Send/SendAmountView.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ struct SendAmountView: View {
174174
maxSendableAmount = nil
175175
}
176176
}
177+
.onChange(of: wallet.selectedFeeRateSatsPerVByte) { _ in
178+
// Recalculate max sendable amount when fee rate becomes available or changes
179+
if app.selectedWalletToPayFrom == .onchain {
180+
Task {
181+
await calculateMaxSendableAmount()
182+
}
183+
}
184+
}
177185
}
178186

179187
private func onContinue() async {
@@ -258,8 +266,8 @@ struct SendAmountView: View {
258266
} catch {
259267
Logger.error("Failed to calculate max sendable amount: \(error)")
260268
await MainActor.run {
261-
// Fall back to total balance if calculation fails
262-
maxSendableAmount = UInt64(wallet.spendableOnchainBalanceSats)
269+
// Keep as nil on error - availableAmount will fall back to total balance
270+
maxSendableAmount = nil
263271
}
264272
}
265273
}

BitkitTests/NumberPadTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,33 @@ final class NumberPadTests: XCTestCase {
179179
XCTAssertEqual(viewModel.amountSats, 0)
180180
}
181181

182+
func testDeleteAfterMaxAmountModernBitcoin() {
183+
let viewModel = AmountInputViewModel()
184+
let currency = mockCurrency(primaryDisplay: .bitcoin, displayUnit: .modern)
185+
186+
// Set max amount using updateFromSats (simulates tapping max button)
187+
// This creates formatted text with spaces (e.g., "100 000 000")
188+
viewModel.updateFromSats(100_000_000, currency: currency)
189+
XCTAssertEqual(viewModel.displayText, "100 000 000")
190+
XCTAssertEqual(viewModel.amountSats, 100_000_000)
191+
192+
// Delete should remove digits, not spaces
193+
// After first delete, should be "10 000 000" (one digit removed)
194+
viewModel.handleNumberPadInput("delete", currency: currency)
195+
XCTAssertEqual(viewModel.displayText, "10 000 000")
196+
XCTAssertEqual(viewModel.amountSats, 10_000_000)
197+
198+
// Delete again
199+
viewModel.handleNumberPadInput("delete", currency: currency)
200+
XCTAssertEqual(viewModel.displayText, "1 000 000")
201+
XCTAssertEqual(viewModel.amountSats, 1_000_000)
202+
203+
// Delete again
204+
viewModel.handleNumberPadInput("delete", currency: currency)
205+
XCTAssertEqual(viewModel.displayText, "100 000")
206+
XCTAssertEqual(viewModel.amountSats, 100_000)
207+
}
208+
182209
// MARK: - Leading Zero Tests
183210

184211
func testLeadingZeroBehavior() {

0 commit comments

Comments
 (0)