Skip to content

Commit 09da128

Browse files
authored
Merge branch 'master' into fix/update-onboarding-test-tag
2 parents abff040 + 8b82e5e commit 09da128

25 files changed

+351
-48
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,5 @@ jobs:
5353
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
5454
5555
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
56-
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
57-
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
58-
56+
# or https://code.claude.com/docs/en/cli-reference for available options
57+
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

.github/workflows/claude.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ jobs:
5050

5151
# Optional: Add claude_args to customize behavior and configuration
5252
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
53-
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
53+
# or https://code.claude.com/docs/en/cli-reference for available options
5454
# claude_args: '--allowed-tools Bash(gh pr:*)'

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
# - { name: onchain_boost_receive_widgets, grep: "@onchain|@boost|@receive|@widgets" }
109109
# - { name: settings, grep: "@settings" }
110110
# - { name: security, grep: "@security" }
111-
- { name: e2e, grep: '@send|@lnurl|@lightning|@backup|@onboarding|@onchain_1|@onchain_2|@numberpad|@widgets|@boost|@receive|@settings|@security' }
111+
- { name: e2e, grep: '@transfer|@send|@lnurl|@lightning|@backup|@onboarding|@onchain_1|@onchain_2|@numberpad|@widgets|@boost|@receive|@settings|@security' }
112112

113113
name: e2e-tests - ${{ matrix.shard.name }}
114114

Bitkit/Components/NumberPadTextField.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ struct NumberPadTextField: View {
7171
}
7272
.contentShape(Rectangle())
7373
.animation(springAnimation, value: currency.primaryDisplay)
74+
.accessibilityElement(children: .contain)
75+
.accessibilityIdentifierIfPresent(testIdentifier)
7476
}
7577

7678
@ViewBuilder
@@ -87,7 +89,6 @@ struct NumberPadTextField: View {
8789
+ Text(viewModel.getPlaceholder(currency: currency))
8890
.foregroundColor(isFocused ? .textSecondary : .textPrimary))
8991
.font(.custom(Fonts.black, size: 44))
90-
.accessibilityIdentifierIfPresent(testIdentifier)
9192
}
9293
}
9394
}

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/ViewModels/ChannelDetailsViewModel.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ class ChannelDetailsViewModel: ObservableObject {
1414
@Published var error: Error? = nil
1515

1616
private let coreService: CoreService
17+
private let transferStorage: TransferStorage
1718

1819
/// Private initializer for the singleton instance
19-
private init(coreService: CoreService = .shared) {
20+
private init(coreService: CoreService = .shared, transferStorage: TransferStorage = .shared) {
2021
self.coreService = coreService
22+
self.transferStorage = transferStorage
2123
}
2224

2325
/// Find a channel by ID, checking open channels, pending channels, pending orders, then closed channels
@@ -124,15 +126,31 @@ class ChannelDetailsViewModel: ObservableObject {
124126
connections.append(contentsOf: channels.filter { !$0.isChannelReady })
125127
}
126128

129+
// Only show pending orders that have been paid (aligns with Android/RN behavior)
130+
let paidOrderIds: Set<String> = {
131+
guard let activeTransfers = try? transferStorage.getActiveTransfers() else {
132+
return []
133+
}
134+
return Set(
135+
activeTransfers
136+
.filter { $0.type.isToSpending() }
137+
.compactMap(\.lspOrderId)
138+
)
139+
}()
140+
141+
if paidOrderIds.isEmpty {
142+
return connections
143+
}
144+
127145
// Create fake channels from pending orders
128146
guard let orders = try? await coreService.blocktank.orders(refresh: false) else {
129147
return connections
130148
}
131149

132-
let pendingOrders = orders.filter { order in
133-
// Include orders that are created or paid but not yet opened
134-
order.state2 == .created || order.state2 == .paid
135-
}
150+
let pendingOrders = Self.pendingOrders(
151+
orders: orders,
152+
paidOrderIds: paidOrderIds
153+
)
136154

137155
for order in pendingOrders {
138156
let fakeChannel = createFakeChannel(from: order)
@@ -142,6 +160,12 @@ class ChannelDetailsViewModel: ObservableObject {
142160
return connections
143161
}
144162

163+
static func pendingOrders(orders: [IBtOrder], paidOrderIds: Set<String>) -> [IBtOrder] {
164+
orders.filter { order in
165+
paidOrderIds.contains(order.id) && (order.state2 == .created || order.state2 == .paid)
166+
}
167+
}
168+
145169
/// Creates a fake channel from a Blocktank order for UI display purposes
146170
private func createFakeChannel(from order: IBtOrder) -> ChannelDetails {
147171
return ChannelDetails(

Bitkit/ViewModels/TransferViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ class TransferViewModel: ObservableObject {
113113
uiState.isAdvanced = true
114114
}
115115

116+
func displayOrder(for order: IBtOrder) -> IBtOrder {
117+
uiState.order ?? order
118+
}
119+
116120
func payOrder(order: IBtOrder, speed: TransactionSpeed) async throws {
117121
var fees = try? await coreService.blocktank.fees(refresh: true)
118122
if fees == nil {

Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,13 @@ struct LightningConnectionsView: View {
7070
.padding(.top, 16)
7171

7272
ForEach(Array(pendingConnections.enumerated()), id: \.element.channelId) { index, channel in
73+
let labelIndex = pendingConnections.count - index
7374
Button {
7475
navigation.navigate(.connectionDetail(channelId: channel.channelIdString))
7576
} label: {
7677
VStack(spacing: 0) {
7778
HStack {
78-
SubtitleText("\(t("lightning__connection")) \(index + 1)")
79+
SubtitleText("\(t("lightning__connection")) \(labelIndex)")
7980
Spacer()
8081
Image("chevron")
8182
.resizable()
@@ -109,12 +110,13 @@ struct LightningConnectionsView: View {
109110
.padding(.top, 16)
110111

111112
ForEach(Array(openChannels.enumerated()), id: \.element.channelId) { index, channel in
113+
let labelIndex = openChannels.count - index
112114
Button {
113115
navigation.navigate(.connectionDetail(channelId: channel.channelIdString))
114116
} label: {
115117
VStack(spacing: 0) {
116118
HStack {
117-
SubtitleText("\(t("lightning__connection")) \(index + 1)")
119+
SubtitleText("\(t("lightning__connection")) \(labelIndex)")
118120
Spacer()
119121
Image("chevron")
120122
.resizable()
@@ -147,12 +149,13 @@ struct LightningConnectionsView: View {
147149
.padding(.top, 16)
148150

149151
ForEach(Array(closedChannels.enumerated()), id: \.element.channelId) { index, channel in
152+
let labelIndex = closedChannels.count - index
150153
Button {
151154
navigation.navigate(.connectionDetail(channelId: channel.channelIdString))
152155
} label: {
153156
VStack(spacing: 0) {
154157
HStack {
155-
SubtitleText("\(t("lightning__connection")) \(index + 1)")
158+
SubtitleText("\(t("lightning__connection")) \(labelIndex)")
156159
Spacer()
157160
Image("chevron")
158161
.resizable()

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 {

0 commit comments

Comments
 (0)