diff --git a/AGENTS.md b/AGENTS.md index cef3228a2..bc65f1448 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -244,8 +244,9 @@ New feature (`TransferTrackingManager`) tracks pending transfers to handle edge ### iOS Version Compatibility -- Xcode previews only work with iOS 17 and below (due to Rust dependencies) -- Use availability checks for iOS 18/26 features: +- Minimum deployment target is **iOS 17.0**. +- Xcode previews work with the minimum target (iOS 17); they may not work on iOS 18+ due to Rust dependencies. +- Use availability checks only for iOS 18+ features: ```swift if #available(iOS 18.0, *) { // Use iOS 18+ features diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 8635c470b..5f02cda35 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -433,15 +433,17 @@ inputFileListPaths = ( ); inputPaths = ( + "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework", ); name = "Remove Static Framework Stubs"; outputFileListPaths = ( ); outputPaths = ( + "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/.ldk-stubs-removed", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\n"; + shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\nOUTPUT_SENTINEL=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/.ldk-stubs-removed\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\ntouch \"$OUTPUT_SENTINEL\"\\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift index 4b0268ce6..134f92c0d 100644 --- a/Bitkit/AppScene.swift +++ b/Bitkit/AppScene.swift @@ -84,11 +84,11 @@ struct AppScene: View { config in ForgotPinSheet(config: config) } .task(priority: .userInitiated, setupTask) - .onChange(of: currency.hasStaleData, perform: handleCurrencyStaleData) - .onChange(of: wallet.walletExists, perform: handleWalletExistsChange) - .onChange(of: wallet.nodeLifecycleState, perform: handleNodeLifecycleChange) - .onChange(of: scenePhase, perform: handleScenePhaseChange) - .onChange(of: migrations.isShowingMigrationLoading) { isLoading in + .onChange(of: currency.hasStaleData) { _, newValue in handleCurrencyStaleData(newValue) } + .onChange(of: wallet.walletExists) { _, newValue in handleWalletExistsChange(newValue) } + .onChange(of: wallet.nodeLifecycleState) { _, newValue in handleNodeLifecycleChange(newValue) } + .onChange(of: scenePhase) { _, newValue in handleScenePhaseChange(newValue) } + .onChange(of: migrations.isShowingMigrationLoading) { _, isLoading in if !isLoading { SettingsViewModel.shared.updatePinEnabledState() widgets.loadSavedWidgets() @@ -107,7 +107,7 @@ struct AppScene: View { } } } - .onChange(of: network.isConnected) { isConnected in + .onChange(of: network.isConnected) { _, isConnected in // Retry starting wallet when network comes back online if isConnected { handleNetworkRestored() diff --git a/Bitkit/Components/Activity/ActivityListFilter.swift b/Bitkit/Components/Activity/ActivityListFilter.swift index 8bf3f979c..a856d9956 100644 --- a/Bitkit/Components/Activity/ActivityListFilter.swift +++ b/Bitkit/Components/Activity/ActivityListFilter.swift @@ -25,8 +25,6 @@ struct ActivityListFilter: View { }) } } - // TODO: uncomment after bump to iOS 18 - // .containerRelativeFrame(.horizontal, alignment: .trailing) } .frame(maxWidth: .infinity) diff --git a/Bitkit/Components/AppStatus.swift b/Bitkit/Components/AppStatus.swift index 6c959f4f7..383dfe526 100644 --- a/Bitkit/Components/AppStatus.swift +++ b/Bitkit/Components/AppStatus.swift @@ -46,7 +46,7 @@ struct AppStatus: View { .onAppear { startAnimations() } - .onChange(of: appStatus) { _ in + .onChange(of: appStatus) { startAnimations() } } diff --git a/Bitkit/Components/Button/Button.swift b/Bitkit/Components/Button/Button.swift index daeb7d171..565837b03 100644 --- a/Bitkit/Components/Button/Button.swift +++ b/Bitkit/Components/Button/Button.swift @@ -32,7 +32,7 @@ struct CustomButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .onChange(of: configuration.isPressed) { pressed in + .onChange(of: configuration.isPressed) { _, pressed in isPressed = pressed } } diff --git a/Bitkit/Components/CustomSlider.swift b/Bitkit/Components/CustomSlider.swift index 149a250d4..a27fcbd46 100644 --- a/Bitkit/Components/CustomSlider.swift +++ b/Bitkit/Components/CustomSlider.swift @@ -70,7 +70,7 @@ struct CustomSlider: View { .onAppear { sliderWidth = geometry.size.width } - .onChange(of: geometry.size.width) { width in + .onChange(of: geometry.size.width) { _, width in sliderWidth = width } } @@ -105,7 +105,7 @@ struct CustomSlider: View { sliderIndex = Double(index) } } - .onChange(of: value) { newValue in + .onChange(of: value) { _, newValue in // Update slider position when value changes externally with animation if let index = steps.firstIndex(of: newValue) { withAnimation(.easeOut(duration: 0.2)) { diff --git a/Bitkit/Components/DrawerView.swift b/Bitkit/Components/DrawerView.swift index 9dd6733a8..28a0f6a28 100644 --- a/Bitkit/Components/DrawerView.swift +++ b/Bitkit/Components/DrawerView.swift @@ -178,7 +178,7 @@ struct DrawerView: View { .transition(.move(edge: .trailing)) } } - .onChange(of: app.showDrawer) { show in + .onChange(of: app.showDrawer) { _, show in if show { currentDragOffset = 0 withAnimation(.easeOut(duration: 0.25)) { diff --git a/Bitkit/Components/IconButton.swift b/Bitkit/Components/IconButton.swift index 70f5db096..b6c96a7d8 100644 --- a/Bitkit/Components/IconButton.swift +++ b/Bitkit/Components/IconButton.swift @@ -5,7 +5,7 @@ private struct IconButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .onChange(of: configuration.isPressed) { pressed in + .onChange(of: configuration.isPressed) { _, pressed in isPressed = pressed } } diff --git a/Bitkit/Components/QR.swift b/Bitkit/Components/QR.swift index 7e99ac481..e0748a038 100644 --- a/Bitkit/Components/QR.swift +++ b/Bitkit/Components/QR.swift @@ -48,7 +48,7 @@ struct QR: View { cachedImage = generateQRCode(from: content) } } - .onChange(of: content) { newContent in + .onChange(of: content) { _, newContent in // Regenerate when content changes cachedContent = newContent cachedImage = generateQRCode(from: newContent) diff --git a/Bitkit/Components/Scanner.swift b/Bitkit/Components/Scanner.swift index b23222d00..8b6b5525f 100644 --- a/Bitkit/Components/Scanner.swift +++ b/Bitkit/Components/Scanner.swift @@ -42,7 +42,7 @@ private struct ScannerCornerButtons: View { .background(Color.white16) .clipShape(Circle()) } - .onChange(of: selectedItem) { item in + .onChange(of: selectedItem) { _, item in Task { await onImageSelection(item) } } diff --git a/Bitkit/Components/SyncNodeView.swift b/Bitkit/Components/SyncNodeView.swift index 0898babc1..1112293b3 100644 --- a/Bitkit/Components/SyncNodeView.swift +++ b/Bitkit/Components/SyncNodeView.swift @@ -80,7 +80,7 @@ struct SyncNodeView: View { .padding(.horizontal, 16) .sheetBackground() .frame(maxWidth: .infinity, maxHeight: .infinity) - .onChange(of: wallet.isSyncingWallet) { newValue in + .onChange(of: wallet.isSyncingWallet) { _, newValue in if !newValue { onSyncComplete?() } diff --git a/Bitkit/Components/Widgets/CalculatorWidget.swift b/Bitkit/Components/Widgets/CalculatorWidget.swift index 91256f541..ee17f0e6d 100644 --- a/Bitkit/Components/Widgets/CalculatorWidget.swift +++ b/Bitkit/Components/Widgets/CalculatorWidget.swift @@ -24,7 +24,7 @@ struct CurrencyInputRow: View { .foregroundColor(.textPrimary) .frame(maxWidth: .infinity) .padding(.leading, 8) - .onChange(of: text, perform: onTextChange) + .onChange(of: text) { _, newValue in onTextChange(newValue) } CaptionBText(label, textColor: .textSecondary) .textCase(.uppercase) @@ -127,7 +127,7 @@ struct CalculatorWidget: View { // Format with trailing zeros when user finishes editing fiatAmount = formatFiatInput(fiatAmount) } - .onChange(of: focusedField) { newFocus in + .onChange(of: focusedField) { _, newFocus in // Format fiat amount when focus leaves the field if newFocus != .fiat && !fiatAmount.isEmpty { fiatAmount = formatFiatInput(fiatAmount) @@ -141,13 +141,10 @@ struct CalculatorWidget: View { updateFiatAmount(from: bitcoinAmount) } } - .onChange( - of: currency.selectedCurrency, - perform: { _ in - // Update fiat amount when currency changes - updateFiatAmount(from: bitcoinAmount) - } - ) + .onChange(of: currency.selectedCurrency) { + // Update fiat amount when currency changes + updateFiatAmount(from: bitcoinAmount) + } } /// Updates fiat amount based on bitcoin input diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index 850a5a776..aeb8a6a01 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -70,10 +70,10 @@ struct PriceWidget: View { .onAppear { fetchPriceData() } - .onChange(of: options.selectedPairs) { _ in + .onChange(of: options.selectedPairs) { fetchPriceData() } - .onChange(of: options.selectedPeriod) { _ in + .onChange(of: options.selectedPeriod) { fetchPriceData() } } diff --git a/Bitkit/Extensions/LightningBalance+Extensions.swift b/Bitkit/Extensions/LightningBalance+Extensions.swift index 5376089c9..03165fa05 100644 --- a/Bitkit/Extensions/LightningBalance+Extensions.swift +++ b/Bitkit/Extensions/LightningBalance+Extensions.swift @@ -6,18 +6,18 @@ extension LightningBalance { /// Get the amount in satoshis for any LightningBalance case var amountSats: UInt64 { switch self { - case let .claimableOnChannelClose(details): - return details.amountSatoshis - case let .claimableAwaitingConfirmations(details): - return details.amountSatoshis - case let .contentiousClaimable(details): - return details.amountSatoshis - case let .maybeTimeoutClaimableHtlc(details): - return details.amountSatoshis - case let .maybePreimageClaimableHtlc(details): - return details.amountSatoshis - case let .counterpartyRevokedOutputClaimable(details): - return details.amountSatoshis + case let .claimableOnChannelClose(_, _, amount, _, _, _, _, _): + return amount + case let .claimableAwaitingConfirmations(_, _, amount, _, _): + return amount + case let .contentiousClaimable(_, _, amount, _, _, _): + return amount + case let .maybeTimeoutClaimableHtlc(_, _, amount, _, _, _): + return amount + case let .maybePreimageClaimableHtlc(_, _, amount, _, _): + return amount + case let .counterpartyRevokedOutputClaimable(_, _, amount): + return amount } } @@ -26,8 +26,8 @@ extension LightningBalance { switch self { case .claimableOnChannelClose: return "Claimable on Channel Close" - case let .claimableAwaitingConfirmations(details): - return "Claimable Awaiting Confirmations (Height: \(details.confirmationHeight))" + case let .claimableAwaitingConfirmations(_, _, _, confirmationHeight, _): + return "Claimable Awaiting Confirmations (Height: \(confirmationHeight))" case .contentiousClaimable: return "Contentious Claimable" case .maybeTimeoutClaimableHtlc: @@ -42,12 +42,12 @@ extension LightningBalance { /// Get the block height at which funds become claimable (for timelocked balances) var claimableAtHeight: UInt32? { switch self { - case let .claimableAwaitingConfirmations(details): - return details.confirmationHeight - case let .contentiousClaimable(details): - return details.timeoutHeight - case let .maybeTimeoutClaimableHtlc(details): - return details.claimableHeight + case let .claimableAwaitingConfirmations(_, _, _, confirmationHeight, _): + return confirmationHeight + case let .contentiousClaimable(_, _, _, timeoutHeight, _, _): + return timeoutHeight + case let .maybeTimeoutClaimableHtlc(_, _, _, claimableHeight, _, _): + return claimableHeight default: return nil } diff --git a/Bitkit/Extensions/TextEditor+DismissOnReturn.swift b/Bitkit/Extensions/TextEditor+DismissOnReturn.swift index a9f98ddc9..020bd9553 100644 --- a/Bitkit/Extensions/TextEditor+DismissOnReturn.swift +++ b/Bitkit/Extensions/TextEditor+DismissOnReturn.swift @@ -6,7 +6,7 @@ private struct DismissKeyboardOnReturnModifier: ViewModifier { func body(content: Content) -> some View { content - .onChange(of: text) { newValue in + .onChange(of: text) { _, newValue in guard isFocused.wrappedValue else { return } if newValue.last == "\n" { text = newValue.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index f3f5725e6..1829d883a 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -167,7 +167,7 @@ struct MainNavView: View { TabBar() DrawerView() } - .onChange(of: scenePhase) { newPhase in + .onChange(of: scenePhase) { _, newPhase in if newPhase == .active { // Update notification permission in case user changed it in OS settings notificationManager.updateNotificationPermission() @@ -177,7 +177,7 @@ struct MainNavView: View { handleClipboard() } } - .onChange(of: notificationManager.authorizationStatus) { newStatus in + .onChange(of: notificationManager.authorizationStatus) { _, newStatus in // Handle notification permission changes if newStatus == .authorized { settings.enableNotifications = true @@ -187,7 +187,7 @@ struct MainNavView: View { notificationManager.unregister() } } - .onChange(of: notificationManager.deviceToken) { token in + .onChange(of: notificationManager.deviceToken) { _, token in // Register with backend if device token changed and notifications are enabled if let token, settings.enableNotifications { Task { @@ -204,7 +204,7 @@ struct MainNavView: View { } } } - .onChange(of: settings.enableNotifications) { newValue in + .onChange(of: settings.enableNotifications) { _, newValue in // Handle notification enable/disable if newValue { // Request permission in case user was not prompted yet diff --git a/Bitkit/Managers/ScannerManager.swift b/Bitkit/Managers/ScannerManager.swift index 530f2a68d..755255cc8 100644 --- a/Bitkit/Managers/ScannerManager.swift +++ b/Bitkit/Managers/ScannerManager.swift @@ -219,9 +219,7 @@ class ScannerManager: ObservableObject { } #if targetEnvironment(simulator) && compiler(>=5.7) - if #available(iOS 16, *) { - request.revision = VNDetectBarcodesRequestRevision1 - } + request.revision = VNDetectBarcodesRequestRevision3 #endif let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift index 70cb27769..54873bd82 100644 --- a/Bitkit/Services/BackupService.swift +++ b/Bitkit/Services/BackupService.swift @@ -143,7 +143,7 @@ class BackupService { } Logger.info("Backup succeeded for: '\(category.rawValue)'", context: "BackupService") - } catch let error as CancellationError { + } catch is CancellationError { updateBackupStatus(category: category) { status in BackupItemStatus( synced: status.synced, @@ -162,7 +162,7 @@ class BackupService { Logger.error("Backup failed for: '\(category.rawValue)': \(error)", context: "BackupService") } - try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) } + _ = try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) } } try? await ServiceQueue.background(.backup) { self.runningBackupTasks[category] = backupTask } @@ -286,23 +286,25 @@ class BackupService { } private func startDataStoreListeners() { - // SETTINGS - SettingsViewModel.shared.settingsPublisher - .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) - .sink { [weak self] _ in - guard let self, !self.shouldSkipBackup() else { return } - markBackupRequired(category: .settings) - } - .store(in: &cancellables) + Task { @MainActor in + // SETTINGS + SettingsViewModel.shared.settingsPublisher + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] _ in + guard let self, !self.shouldSkipBackup() else { return } + markBackupRequired(category: .settings) + } + .store(in: &self.cancellables) - // WIDGETS - SettingsViewModel.shared.widgetsPublisher - .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) - .sink { [weak self] _ in - guard let self, !self.shouldSkipBackup() else { return } - markBackupRequired(category: .widgets) - } - .store(in: &cancellables) + // WIDGETS + SettingsViewModel.shared.widgetsPublisher + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] _ in + guard let self, !self.shouldSkipBackup() else { return } + markBackupRequired(category: .widgets) + } + .store(in: &self.cancellables) + } // TRANSFERS TransferStorage.shared.transfersChangedPublisher @@ -332,13 +334,15 @@ class BackupService { .store(in: &cancellables) // APP STATE (UserDefaults changes, etc.) - SettingsViewModel.shared.appStatePublisher - .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) - .sink { [weak self] _ in - guard let self, !self.shouldSkipBackup() else { return } - markBackupRequired(category: .metadata) - } - .store(in: &cancellables) + Task { @MainActor in + SettingsViewModel.shared.appStatePublisher + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] _ in + guard let self, !self.shouldSkipBackup() else { return } + markBackupRequired(category: .metadata) + } + .store(in: &self.cancellables) + } // BLOCKTANK CoreService.shared.blocktank.stateChangedPublisher @@ -510,28 +514,23 @@ class BackupService { } func getLatestBackupTime() async -> UInt64? { - do { - let timestamps = await withTaskGroup(of: UInt64?.self) { group in - for category in BackupCategory.allCases where category != .lightningConnections { - group.addTask { - await self.getRemoteBackupTimestamp(category: category) - } + let timestamps = await withTaskGroup(of: UInt64?.self) { group in + for category in BackupCategory.allCases where category != .lightningConnections { + group.addTask { + await self.getRemoteBackupTimestamp(category: category) } + } - var results: [UInt64] = [] - for await timestamp in group { - if let ts = timestamp, ts > 0 { - results.append(ts) - } + var results: [UInt64] = [] + for await timestamp in group { + if let ts = timestamp, ts > 0 { + results.append(ts) } - return results } - - return timestamps.max() - } catch { - Logger.warn("Failed to get VSS backup timestamp: \(error)", context: "BackupService") - return nil + return results } + + return timestamps.max() } private func getRemoteBackupTimestamp(category: BackupCategory) async -> UInt64? { diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index bb84cbfc5..d871d1092 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -52,8 +52,9 @@ class ActivityService { txIds.formUnion(onchain.boostTxIds) } } + let txIdsToCache = txIds await MainActor.run { - self.cachedTxIdsInBoostTxIds = txIds + self.cachedTxIdsInBoostTxIds = txIdsToCache } } catch { Logger.error("Failed to refresh boostTxIds cache: \(error)", context: "ActivityService") @@ -108,7 +109,7 @@ class ActivityService { func isActivitySeen(id: String) async -> Bool { do { - if let activity = try await getActivityById(activityId: id) { + if let activity = try getActivityById(activityId: id) { switch activity { case let .onchain(onchain): return onchain.seenAt != nil @@ -657,7 +658,7 @@ class ActivityService { } private func processLightningPayment(_ payment: PaymentDetails) async throws { - guard case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind else { return } + guard case let .bolt11(_, preimage, _, description, bolt11) = payment.kind else { return } // Skip pending inbound payments - just means they created an invoice guard !(payment.status == .pending && payment.direction == .inbound) else { return } @@ -712,46 +713,32 @@ class ActivityService { var latestCaughtError: Error? for payment in payments { - do { - let state: BitkitCore.PaymentState = switch payment.status { - case .failed: - .failed - case .pending: - .pending - case .succeeded: - .succeeded - } - - if case let .onchain(txid, _) = payment.kind { - do { - let hadExistingActivity = try getActivityById(activityId: payment.id) != nil - try await self.processOnchainPayment(payment, transactionDetails: nil) - if hadExistingActivity { - updatedCount += 1 - } else { - addedCount += 1 - } - } catch { - Logger.error("Error processing onchain payment \(txid): \(error)", context: "CoreService.syncLdkNodePayments") - latestCaughtError = error + if case let .onchain(txid, _) = payment.kind { + do { + let hadExistingActivity = try getActivityById(activityId: payment.id) != nil + try await self.processOnchainPayment(payment, transactionDetails: nil) + if hadExistingActivity { + updatedCount += 1 + } else { + addedCount += 1 } - } else if case .bolt11 = payment.kind { - do { - let hadExistingActivity = try getActivityById(activityId: payment.id) != nil - try await self.processLightningPayment(payment) - if hadExistingActivity { - updatedCount += 1 - } else { - addedCount += 1 - } - } catch { - Logger.error("Error processing lightning payment \(payment.id): \(error)", context: "CoreService.syncLdkNodePayments") - latestCaughtError = error + } catch { + Logger.error("Error processing onchain payment \(txid): \(error)", context: "CoreService.syncLdkNodePayments") + latestCaughtError = error + } + } else if case .bolt11 = payment.kind { + do { + let hadExistingActivity = try getActivityById(activityId: payment.id) != nil + try await self.processLightningPayment(payment) + if hadExistingActivity { + updatedCount += 1 + } else { + addedCount += 1 } + } catch { + Logger.error("Error processing lightning payment \(payment.id): \(error)", context: "CoreService.syncLdkNodePayments") + latestCaughtError = error } - } catch { - Logger.error("Error syncing LDK payment: \(error)", context: "CoreService") - latestCaughtError = error } } @@ -803,7 +790,7 @@ class ActivityService { } } - let closedChannels = try await getAllClosedChannels(sortDirection: .desc) + let closedChannels = try getAllClosedChannels(sortDirection: .desc) guard !closedChannels.isEmpty else { return nil } let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails(txid: txid) } @@ -1648,6 +1635,7 @@ class BlocktankService { stateChangedSubject.send() } + @discardableResult func open(orderId: String) async throws -> IBtOrder { guard let nodeId = LightningService.shared.nodeId else { throw AppError(serviceError: .nodeNotStarted) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 4a625f461..c8be398a8 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -617,7 +617,7 @@ class LightningService { } func closeChannel(_ channel: ChannelDetails, force: Bool = false, forceCloseReason: String? = nil) async throws { - guard let node else { + guard node != nil else { throw AppError(serviceError: .nodeNotStarted) } @@ -1106,7 +1106,7 @@ extension LightningService { onEvent?(event) switch event { - case let .paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat): + case let .paymentSuccessful(paymentId, paymentHash, _, feePaidMsat): Logger.info("✅ Payment successful: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) feePaidMsat: \(feePaidMsat ?? 0)") Task { let hash = paymentId ?? paymentHash @@ -1131,7 +1131,7 @@ extension LightningService { Logger.warn("No paymentId or paymentHash available for failed payment", context: "LightningService") } } - case let .paymentReceived(paymentId, paymentHash, amountMsat, feePaidMsat): + case let .paymentReceived(paymentId, paymentHash, amountMsat, _): Logger.info("🤑 Payment received: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) amountMsat: \(amountMsat)") Task { let hash = paymentId ?? paymentHash @@ -1141,7 +1141,7 @@ extension LightningService { Logger.error("Failed to handle payment received for \(hash): \(error)", context: "LightningService") } } - case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, claimDeadline, customRecords): + case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, _, _): Logger.info( "🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)" ) @@ -1170,7 +1170,7 @@ extension LightningService { if let channel { await registerClosedChannel(channel: channel, reason: reasonString) - await MainActor.run { + _ = await MainActor.run { channelCache.removeValue(forKey: channelIdString) } } else { @@ -1193,7 +1193,7 @@ extension LightningService { Logger.error("Failed to handle transaction received for \(txid): \(error)", context: "LightningService") } } - case let .onchainTransactionConfirmed(txid, blockHash, blockHeight, confirmationTime, details): + case let .onchainTransactionConfirmed(txid, _, blockHeight, _, details): Logger.info("✅ Onchain transaction confirmed: txid=\(txid) blockHeight=\(blockHeight) amountSats=\(details.amountSats)") Task { do { @@ -1247,7 +1247,7 @@ extension LightningService { // MARK: Balance Events - case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, oldTotalOnchain, newTotalOnchain, oldLightning, newLightning): + case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, _, _, oldLightning, newLightning): Logger .info("💰 Balance changed: onchain=\(oldSpendableOnchain)->\(newSpendableOnchain) lightning=\(oldLightning)->\(newLightning)") diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index aaea3c213..13d847195 100644 --- a/Bitkit/Services/MigrationsService.swift +++ b/Bitkit/Services/MigrationsService.swift @@ -800,7 +800,7 @@ extension MigrationsService { if order.state2 != .paid { try? TransferStorage.shared.markSettled(id: transfer.id, settledAt: now) Logger.info( - "Cleanup: settled invalid migration transfer \(transfer.id) for order \(orderId) (state: \(order.state2))", + "Cleanup: settled invalid migration transfer \(transfer.id) for order \(orderId) (state: \(String(describing: order.state2)))", context: "Migration" ) } @@ -2429,7 +2429,7 @@ extension MigrationsService { // Only create transfers for orders actually paid and awaiting channel guard order.state2 == .paid else { - Logger.debug("Skipping order \(orderId) with state \(order.state2) for transfer creation", context: "Migration") + Logger.debug("Skipping order \(orderId) with state \(String(describing: order.state2)) for transfer creation", context: "Migration") continue } diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index 98ba0e7bb..31a407d0e 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -234,7 +234,7 @@ class TransferService { var orders: [IBtOrder]? = nil do { - orders = try? await blocktankService.orders(orderIds: [orderId], filter: nil, refresh: false) + orders = try await blocktankService.orders(orderIds: [orderId], filter: nil, refresh: false) } catch { Logger.error("Failed to fetch Blocktank orders for orderId \(orderId): \(error)", context: "TransferService") return nil diff --git a/Bitkit/Utilities/Errors.swift b/Bitkit/Utilities/Errors.swift index e4b1f62d6..7ccf91c7b 100644 --- a/Bitkit/Utilities/Errors.swift +++ b/Bitkit/Utilities/Errors.swift @@ -239,9 +239,6 @@ struct AppError: LocalizedError { // message = "Failed to send payment. \(ldkMessage)" message = ldkMessage debugMessage = ldkMessage - case let .InvalidCustomTlvs(message: ldkMessage): - message = "Invalid custom TLVs" - debugMessage = ldkMessage case let .ProbeSendingFailed(message: ldkMessage): message = "Failed to send probe" debugMessage = ldkMessage diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 491a98899..98e23cb34 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -730,7 +730,7 @@ extension AppViewModel { extension AppViewModel { func handleLdkNodeEvent(_ event: Event) { switch event { - case let .paymentReceived(paymentId, paymentHash, amountMsat, customRecords): + case let .paymentReceived(paymentId, _, amountMsat, _): Task { if let paymentId { if await CoreService.shared.activity.isActivitySeen(id: paymentId) { @@ -862,7 +862,7 @@ extension AppViewModel { } } } - case let .onchainTransactionConfirmed(txid, blockHash, blockHeight, confirmationTime, details): + case let .onchainTransactionConfirmed(txid, _, blockHeight, _, _): Logger.info("Transaction confirmed: \(txid) at block \(blockHeight)") case let .onchainTransactionReplaced(txid, conflicts): Logger.info("Transaction replaced: \(txid) by \(conflicts.count) conflict(s)") @@ -922,7 +922,7 @@ extension AppViewModel { // MARK: Sync Events - case let .syncProgress(syncType, progressPercent, currentBlockHeight, targetBlockHeight): + case let .syncProgress(syncType, progressPercent, _, _): Logger.debug("Sync progress: \(syncType) \(progressPercent)%") case let .syncCompleted(syncType, syncedBlockHeight): Logger.info("Sync completed: \(syncType) at height \(syncedBlockHeight)") @@ -963,7 +963,7 @@ extension AppViewModel { // MARK: Balance Events - case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, oldTotalOnchain, newTotalOnchain, oldLightning, newLightning): + case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, _, _, oldLightning, newLightning): Logger.debug("Balance changed: onchain \(oldSpendableOnchain)->\(newSpendableOnchain) lightning \(oldLightning)->\(newLightning)") } } diff --git a/Bitkit/ViewModels/BlocktankViewModel.swift b/Bitkit/ViewModels/BlocktankViewModel.swift index 46397d263..1c9f197c0 100644 --- a/Bitkit/ViewModels/BlocktankViewModel.swift +++ b/Bitkit/ViewModels/BlocktankViewModel.swift @@ -40,7 +40,7 @@ class BlocktankViewModel: ObservableObject { } deinit { - RunLoop.main.perform { [weak self] in + Task { @MainActor [weak self] in Logger.debug("Stopping poll for orders") self?.stopPolling() } @@ -50,11 +50,13 @@ class BlocktankViewModel: ObservableObject { stopPolling() refreshTimer = Timer.scheduledTimer(withTimeInterval: Env.blocktankOrderRefreshInterval, repeats: true) { [weak self] _ in - guard let self else { return } - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in + Task { @MainActor [weak self] in guard let self else { return } - try? await refreshOrders() + refreshTask?.cancel() + refreshTask = Task { @MainActor [weak self] in + guard let self else { return } + try? await refreshOrders() + } } } diff --git a/Bitkit/ViewModels/CurrencyViewModel.swift b/Bitkit/ViewModels/CurrencyViewModel.swift index 873cad5c4..bf2d0ee37 100644 --- a/Bitkit/ViewModels/CurrencyViewModel.swift +++ b/Bitkit/ViewModels/CurrencyViewModel.swift @@ -33,7 +33,7 @@ class CurrencyViewModel: ObservableObject { } deinit { - RunLoop.main.perform { [weak self] in + Task { @MainActor [weak self] in Logger.debug("Stopping poll for rates") self?.stopPolling() } @@ -65,11 +65,13 @@ class CurrencyViewModel: ObservableObject { stopPolling() refreshTimer = Timer.scheduledTimer(withTimeInterval: Env.fxRateRefreshInterval, repeats: true) { [weak self] _ in - guard let self else { return } - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in + Task { @MainActor [weak self] in guard let self else { return } - await refresh() + refreshTask?.cancel() + refreshTask = Task { @MainActor [weak self] in + guard let self else { return } + await refresh() + } } } diff --git a/Bitkit/ViewModels/SettingsViewModel.swift b/Bitkit/ViewModels/SettingsViewModel.swift index 86225e043..86042849c 100644 --- a/Bitkit/ViewModels/SettingsViewModel.swift +++ b/Bitkit/ViewModels/SettingsViewModel.swift @@ -58,7 +58,7 @@ class SettingsViewModel: NSObject, ObservableObject { private let widgetsSubject = PassthroughSubject() private let appStateSubject = PassthroughSubject() - nonisolated var settingsPublisher: AnyPublisher<[String: Any], Never> { + var settingsPublisher: AnyPublisher<[String: Any], Never> { settingsSubject .removeDuplicates { old, new in NSDictionary(dictionary: old).isEqual(to: new) @@ -66,7 +66,7 @@ class SettingsViewModel: NSObject, ObservableObject { .eraseToAnyPublisher() } - nonisolated var widgetsPublisher: AnyPublisher { + var widgetsPublisher: AnyPublisher { widgetsSubject .removeDuplicates { old, new in if let old, let new { @@ -77,7 +77,7 @@ class SettingsViewModel: NSObject, ObservableObject { .eraseToAnyPublisher() } - nonisolated var appStatePublisher: AnyPublisher { + var appStatePublisher: AnyPublisher { appStateSubject.eraseToAnyPublisher() } @@ -108,7 +108,7 @@ class SettingsViewModel: NSObject, ObservableObject { @AppStorage("ignoresHideBalanceToast") var ignoresHideBalanceToast: Bool = false // PIN Management - @Published internal(set) var pinEnabled: Bool = false + @Published var pinEnabled: Bool = false @AppStorage("pinFailedAttempts") var pinFailedAttempts: Int = 0 @AppStorage("requirePinForPayments") var requirePinForPayments: Bool = false @AppStorage("useBiometrics") var useBiometrics: Bool = false @@ -175,17 +175,21 @@ class SettingsViewModel: NSObject, ObservableObject { } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - if SettingsBackupConfig.settingsKeys.contains(keyPath ?? "") { - settingsSubject.send(getSettingsDictionary()) - } else if keyPath == "savedWidgets" { - widgetsSubject.send(defaults.data(forKey: "savedWidgets")) - } else if SettingsBackupConfig.appStateKeys.contains(keyPath ?? "") { - appStateSubject.send() + Task { @MainActor in + if SettingsBackupConfig.settingsKeys.contains(keyPath ?? "") { + settingsSubject.send(getSettingsDictionary()) + } else if keyPath == "savedWidgets" { + widgetsSubject.send(defaults.data(forKey: "savedWidgets")) + } else if SettingsBackupConfig.appStateKeys.contains(keyPath ?? "") { + appStateSubject.send() + } } } nonisolated func notifyAppStateChanged() { - appStateSubject.send() + Task { @MainActor in + appStateSubject.send() + } } /// Call after removePersistentDomain; singleton retains stale @AppStorage values. @@ -756,7 +760,7 @@ class SettingsViewModel: NSObject, ObservableObject { /// Re-read UserDefaults into @AppStorage properties after a direct defaults write. private func syncAppStorageFromDefaults() { _swipeBalanceToHide = defaults.object(forKey: "swipeBalanceToHide") as? Bool ?? true - defaultTransactionSpeed = TransactionSpeed(rawValue: defaults.string(forKey: "defaultTransactionSpeed") ?? "") ?? .normal + defaultTransactionSpeed = TransactionSpeed(rawValue: defaults.string(forKey: "defaultTransactionSpeed") ?? "") hideBalance = defaults.bool(forKey: "hideBalance") hideBalanceOnOpen = defaults.bool(forKey: "hideBalanceOnOpen") readClipboard = defaults.bool(forKey: "readClipboard") diff --git a/Bitkit/ViewModels/TransferViewModel.swift b/Bitkit/ViewModels/TransferViewModel.swift index 247f16b26..c93dbff66 100644 --- a/Bitkit/ViewModels/TransferViewModel.swift +++ b/Bitkit/ViewModels/TransferViewModel.swift @@ -75,7 +75,7 @@ class TransferViewModel: ObservableObject { } deinit { - RunLoop.main.perform { [weak self] in + Task { @MainActor [weak self] in Logger.debug("Stopping poll for order") self?.stopPolling() } @@ -210,41 +210,43 @@ class TransferViewModel: ObservableObject { Logger.debug("Starting to watch order \(orderId)") refreshTimer = Timer.scheduledTimer(withTimeInterval: frequencySecs, repeats: true) { [weak self] _ in - guard let self else { return } - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in + Task { @MainActor [weak self] in guard let self else { return } + refreshTask?.cancel() + refreshTask = Task { @MainActor [weak self] in + guard let self else { return } - do { - Logger.debug("Refreshing order \(orderId)") - let orders = try await coreService.blocktank.orders(orderIds: [orderId], refresh: true) - guard let order = orders.first else { - Logger.error("Order not found \(orderId)", context: "TransferViewModel") - return - } - - let step = try await updateOrder(order: order) - lightningSetupStep = step - Logger.debug("LN setup step: \(step)") - - if order.state2 == .expired { - Logger.error("Order expired \(orderId)", context: "TransferViewModel") - stopPolling() - return - } - - if step > 2 { - Logger.debug("Order settled, stopping polling") - - // Sync transfer states when order completes - try? await transferService.syncTransferStates() - + do { + Logger.debug("Refreshing order \(orderId)") + let orders = try await coreService.blocktank.orders(orderIds: [orderId], refresh: true) + guard let order = orders.first else { + Logger.error("Order not found \(orderId)", context: "TransferViewModel") + return + } + + let step = try await updateOrder(order: order) + lightningSetupStep = step + Logger.debug("LN setup step: \(step)") + + if order.state2 == .expired { + Logger.error("Order expired \(orderId)", context: "TransferViewModel") + stopPolling() + return + } + + if step > 2 { + Logger.debug("Order settled, stopping polling") + + // Sync transfer states when order completes + try? await transferService.syncTransferStates() + + stopPolling() + return + } + } catch { + Logger.error(error, context: "Failed to watch order") stopPolling() - return } - } catch { - Logger.error(error, context: "Failed to watch order") - stopPolling() } } } diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 6ea9d86b6..84d686463 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -198,7 +198,7 @@ class WalletViewModel: ObservableObject { // MARK: Sync Events - case let .syncProgress(syncType, progressPercent, syncCurrentBlockHeight, targetBlockHeight): + case let .syncProgress(syncType, progressPercent, syncCurrentBlockHeight, _): self.isSyncingWallet = true if syncCurrentBlockHeight > self.currentBlockHeight { self.currentBlockHeight = syncCurrentBlockHeight @@ -351,6 +351,7 @@ class WalletViewModel: ObservableObject { return invoice.lowercased() } + @discardableResult func waitForNodeToRun(timeoutSeconds: Double = 10.0) async -> Bool { guard nodeLifecycleState != .running else { return true } @@ -1002,7 +1003,7 @@ class WalletViewModel: ObservableObject { func wipe() async throws { Logger.warn("Starting wallet wipe", context: "WalletViewModel") - _ = await waitForNodeToRun(timeoutSeconds: 5.0) + await waitForNodeToRun(timeoutSeconds: 5.0) if nodeLifecycleState == .starting || nodeLifecycleState == .running { try await stopLightningNode(clearEventCallback: true) diff --git a/Bitkit/Views/Gift/GiftLoading.swift b/Bitkit/Views/Gift/GiftLoading.swift index 94bd2fd92..f8127444a 100644 --- a/Bitkit/Views/Gift/GiftLoading.swift +++ b/Bitkit/Views/Gift/GiftLoading.swift @@ -62,7 +62,7 @@ struct GiftLoading: View { // Wait a bit for peers to connect if node is starting guard wallet.nodeLifecycleState == .running else { // Wait for node to be running - _ = await wallet.waitForNodeToRun(timeoutSeconds: 30.0) + await wallet.waitForNodeToRun(timeoutSeconds: 30.0) return } diff --git a/Bitkit/Views/Onboarding/InitializingWalletView.swift b/Bitkit/Views/Onboarding/InitializingWalletView.swift index 894d90d43..849ba2275 100644 --- a/Bitkit/Views/Onboarding/InitializingWalletView.swift +++ b/Bitkit/Views/Onboarding/InitializingWalletView.swift @@ -115,13 +115,13 @@ struct InitializingWalletView: View { } } } - .onChange(of: shouldFinish) { finish in + .onChange(of: shouldFinish) { _, finish in if finish && percentage >= 99.9 { percentage = 100 handleCompletion() } } - .onChange(of: percentage) { newPercentage in + .onChange(of: percentage) { _, newPercentage in if newPercentage >= 99.9 && shouldFinish { percentage = 100 handleCompletion() diff --git a/Bitkit/Views/Onboarding/RestoreWalletView.swift b/Bitkit/Views/Onboarding/RestoreWalletView.swift index c06d39585..27c4f982d 100644 --- a/Bitkit/Views/Onboarding/RestoreWalletView.swift +++ b/Bitkit/Views/Onboarding/RestoreWalletView.swift @@ -145,14 +145,14 @@ struct RestoreWalletView: View { HStack(alignment: .top, spacing: 4) { // First column (1-6 or 1-12) VStack(spacing: 4) { - ForEach(0 ..< wordsPerColumn) { index in + ForEach(Array(0 ..< wordsPerColumn), id: \.self) { index in SeedTextField( index: index, text: index == 0 ? $firstFieldText : $words[index], isLastField: index == (wordsPerColumn * 2 - 1), focusedField: $focusedField ) - .onChange(of: firstFieldText) { newValue in + .onChange(of: firstFieldText) { _, newValue in if index == 0 && newValue.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { handlePastedWords(newValue) } else if index == 0 { @@ -164,7 +164,7 @@ struct RestoreWalletView: View { // Second column (7-12 or 13-24) VStack(spacing: 4) { - ForEach(wordsPerColumn ..< (wordsPerColumn * 2)) { index in + ForEach(Array(wordsPerColumn ..< (wordsPerColumn * 2)), id: \.self) { index in SeedTextField( index: index, text: $words[index], diff --git a/Bitkit/Views/Security/PinCheckView.swift b/Bitkit/Views/Security/PinCheckView.swift index 413d8ae8c..a4805f469 100644 --- a/Bitkit/Views/Security/PinCheckView.swift +++ b/Bitkit/Views/Security/PinCheckView.swift @@ -40,11 +40,7 @@ struct PinCheckView: View { if settings.hasExceededPinAttempts() { // Exceeded maximum attempts - this should be handled by the app level - let remainingAttempts = settings.getRemainingPinAttempts() - errorMessage = t( - "security__pin_exceeded_attempts", - comment: "Too many incorrect attempts. Please try again later." - ) + // TODO: wipe app errorIdentifier = "WrongPIN" return } @@ -53,18 +49,11 @@ struct PinCheckView: View { if remainingAttempts == 1 { // Last attempt warning - errorMessage = t( - "security__pin_last_attempt", - comment: "Last attempt. Entering the wrong PIN again will reset your wallet." - ) + errorMessage = t("security__pin_last_attempt") errorIdentifier = "LastAttempt" } else { // Show remaining attempts - errorMessage = t( - "security__pin_attempts", - comment: "%d attempts remaining. Forgot your PIN?", - variables: ["attemptsRemaining": "\(remainingAttempts)"] - ) + errorMessage = t("security__pin_attempts", variables: ["attemptsRemaining": "\(remainingAttempts)"]) errorIdentifier = "AttemptsRemaining" } } diff --git a/Bitkit/Views/Settings/Advanced/AddressViewer.swift b/Bitkit/Views/Settings/Advanced/AddressViewer.swift index 129ec7f84..536d7fbb9 100644 --- a/Bitkit/Views/Settings/Advanced/AddressViewer.swift +++ b/Bitkit/Views/Settings/Advanced/AddressViewer.swift @@ -199,7 +199,7 @@ struct AddressViewer: View { .frame(height: 80) } } - .onChange(of: selectedAddressKind) { _ in + .onChange(of: selectedAddressKind) { selectedAddress = "" withAnimation(.easeInOut(duration: 0.5)) { proxy.scrollTo("top", anchor: .top) @@ -209,7 +209,7 @@ struct AddressViewer: View { await loadAddresses() } } - .onChange(of: selectedScriptType) { _ in + .onChange(of: selectedScriptType) { selectedAddress = "" withAnimation(.easeInOut(duration: 0.5)) { proxy.scrollTo("top", anchor: .top) @@ -219,7 +219,7 @@ struct AddressViewer: View { await loadAddresses() } } - .onChange(of: searchText) { _ in + .onChange(of: searchText) { // Reset scroll position when searching withAnimation(.easeInOut(duration: 0.5)) { proxy.scrollTo("top", anchor: .top) diff --git a/Bitkit/Views/Settings/LogView.swift b/Bitkit/Views/Settings/LogView.swift index 02705c5c9..8b2187529 100644 --- a/Bitkit/Views/Settings/LogView.swift +++ b/Bitkit/Views/Settings/LogView.swift @@ -129,7 +129,7 @@ struct LogContentView: View { .frame(maxWidth: .infinity, alignment: .leading) } } - .onChange(of: shouldScrollToBottom) { _ in + .onChange(of: shouldScrollToBottom) { scrollToBottom(proxy: proxy) } } diff --git a/Bitkit/Views/Transfer/SavingsProgressView.swift b/Bitkit/Views/Transfer/SavingsProgressView.swift index 06182ad66..8abeaa670 100644 --- a/Bitkit/Views/Transfer/SavingsProgressView.swift +++ b/Bitkit/Views/Transfer/SavingsProgressView.swift @@ -173,7 +173,7 @@ struct SavingsProgressView: View { // Ensure we re-enable screen timeout when view disappears UIApplication.shared.isIdleTimerDisabled = false } - .onChange(of: transfer.transferUnavailable) { unavailable in + .onChange(of: transfer.transferUnavailable) { _, unavailable in if unavailable { transfer.transferUnavailable = false app.toast( diff --git a/Bitkit/Views/Transfer/SpendingAdvancedView.swift b/Bitkit/Views/Transfer/SpendingAdvancedView.swift index 33b6b85f6..825135f64 100644 --- a/Bitkit/Views/Transfer/SpendingAdvancedView.swift +++ b/Bitkit/Views/Transfer/SpendingAdvancedView.swift @@ -109,7 +109,7 @@ struct SpendingAdvancedView: View { updateFeeEstimate() } - .onChange(of: lspBalance) { _ in + .onChange(of: lspBalance) { if isValid { updateFeeEstimate() } else { diff --git a/Bitkit/Views/Transfer/SpendingAmount.swift b/Bitkit/Views/Transfer/SpendingAmount.swift index 6ab372de9..750832191 100644 --- a/Bitkit/Views/Transfer/SpendingAmount.swift +++ b/Bitkit/Views/Transfer/SpendingAmount.swift @@ -80,7 +80,7 @@ struct SpendingAmount: View { .task(id: blocktank.info?.options.maxChannelSizeSat) { await calculateMaxTransferAmount() } - .onChange(of: wallet.spendableOnchainBalanceSats) { _ in + .onChange(of: wallet.spendableOnchainBalanceSats) { Task { await calculateMaxTransferAmount() } diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index 8b09e143d..8053984a9 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -217,7 +217,7 @@ struct ActivityItemView: View { } .navigationBarHidden(true) .padding(.horizontal, 16) - .onChange(of: sheets.addTagSheetItem) { item in + .onChange(of: sheets.addTagSheetItem) { _, item in if item == nil { // Add tag sheet was closed, reload tags in case they were modified Task { @@ -225,7 +225,7 @@ struct ActivityItemView: View { } } } - .onChange(of: sheets.boostSheetItem) { item in + .onChange(of: sheets.boostSheetItem) { _, item in if item == nil { // Boost sheet was closed, reload activity in case it was boosted Task { diff --git a/Bitkit/Views/Wallets/Receive/QrArea.swift b/Bitkit/Views/Wallets/Receive/QrArea.swift index 54bde31b9..6d6c4e980 100644 --- a/Bitkit/Views/Wallets/Receive/QrArea.swift +++ b/Bitkit/Views/Wallets/Receive/QrArea.swift @@ -66,12 +66,8 @@ struct QrArea: View { .sheet(isPresented: $showShareSheet) { ShareSheet(activityItems: shareItems) } - .onAppear { - // Pre-generate the QR image for sharing - shareQRImage = generateShareQrImage() - } - .onChange(of: uri) { _ in - // Regenerate when URI changes + .onChange(of: uri, initial: true) { + // Generate the QR image for sharing shareQRImage = generateShareQrImage() } } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 03d5f3a5f..9e7e8ddfa 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -134,7 +134,7 @@ struct ReceiveQr: View { } await app.checkGeoStatus() } - .onChange(of: wallet.nodeLifecycleState) { newState in + .onChange(of: wallet.nodeLifecycleState) { _, newState in // They may open this view before node has started if newState == .running { Task { diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 250b0fefa..5c823477c 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -80,7 +80,7 @@ struct LnurlPayConfirm: View { .focused($isCommentFocused) .dismissKeyboardOnReturn(text: $comment, isFocused: $isCommentFocused) .lineLimit(3 ... 3) - .onChange(of: comment) { newValue in + .onChange(of: comment) { _, newValue in let maxLength = Int(commentAllowed) if newValue.count > maxLength { comment = String(newValue.prefix(maxLength)) diff --git a/Bitkit/Views/Wallets/Send/SendAmountView.swift b/Bitkit/Views/Wallets/Send/SendAmountView.swift index 1bfe9ae0d..7ff3eda8a 100644 --- a/Bitkit/Views/Wallets/Send/SendAmountView.swift +++ b/Bitkit/Views/Wallets/Send/SendAmountView.swift @@ -160,7 +160,7 @@ struct SendAmountView: View { } } } - .onChange(of: app.selectedWalletToPayFrom) { newValue in + .onChange(of: app.selectedWalletToPayFrom) { _, newValue in // Recalculate max sendable amount when switching wallet types if newValue == .onchain { Task { @@ -174,7 +174,7 @@ struct SendAmountView: View { maxSendableAmount = nil } } - .onChange(of: wallet.selectedFeeRateSatsPerVByte) { _ in + .onChange(of: wallet.selectedFeeRateSatsPerVByte) { // Recalculate max sendable amount when fee rate becomes available or changes if app.selectedWalletToPayFrom == .onchain { Task { diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index 8853cd29e..e6da1d454 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -161,7 +161,7 @@ struct SendConfirmationView: View { await calculateTransactionFee() await calculateRoutingFee() } - .onChange(of: wallet.selectedFeeRateSatsPerVByte) { _ in + .onChange(of: wallet.selectedFeeRateSatsPerVByte) { Task { await calculateTransactionFee() } diff --git a/Bitkit/Views/Wallets/Send/SendFeeRate.swift b/Bitkit/Views/Wallets/Send/SendFeeRate.swift index 781eca395..a4a081c75 100644 --- a/Bitkit/Views/Wallets/Send/SendFeeRate.swift +++ b/Bitkit/Views/Wallets/Send/SendFeeRate.swift @@ -157,7 +157,7 @@ struct SendFeeRate: View { .task { await loadFeeEstimates() } - .onChange(of: wallet.selectedFeeRateSatsPerVByte) { _ in + .onChange(of: wallet.selectedFeeRateSatsPerVByte) { Task { await calculateTransactionFees() } diff --git a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift index 5af85d10d..5340d2a49 100644 --- a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift +++ b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift @@ -77,7 +77,7 @@ struct SendPendingScreen: View { .task { await searchForActivity() } - .onChange(of: app.sendSheetPendingResolution) { resolution in + .onChange(of: app.sendSheetPendingResolution) { _, resolution in guard let resolution, resolution.paymentHash == paymentHash else { return } app.consumeSendSheetPendingResolution(paymentHash: paymentHash) if resolution.success { diff --git a/Bitkit/Views/Wallets/Send/SendSheet.swift b/Bitkit/Views/Wallets/Send/SendSheet.swift index 189e622fe..8318f8e10 100644 --- a/Bitkit/Views/Wallets/Send/SendSheet.swift +++ b/Bitkit/Views/Wallets/Send/SendSheet.swift @@ -106,7 +106,7 @@ struct SendSheet: View { } } } - .onChange(of: wallet.nodeLifecycleState) { state in + .onChange(of: wallet.nodeLifecycleState) { _, state in // When the node becomes running and we have a scanned invoice, run deferred validation. // This covers: // - Pure onchain invoices (node was not running at scan time) @@ -121,7 +121,7 @@ struct SendSheet: View { validatePaymentAfterSync() } } - .onChange(of: wallet.hasUsableChannels) { hasUsable in + .onChange(of: wallet.hasUsableChannels) { _, hasUsable in // Only validate if channels just became usable and we have a scanned invoice // (Validation already happened in AppViewModel if channels were already usable) let hasScannedInvoice = app.scannedLightningInvoice != nil || app.scannedOnchainInvoice != nil || app.lnurlPayData != nil diff --git a/Bitkit/Views/Wallets/Send/SendUtxoSelectionView.swift b/Bitkit/Views/Wallets/Send/SendUtxoSelectionView.swift index ab3f7ef8f..0f7a6a95b 100644 --- a/Bitkit/Views/Wallets/Send/SendUtxoSelectionView.swift +++ b/Bitkit/Views/Wallets/Send/SendUtxoSelectionView.swift @@ -67,14 +67,8 @@ struct SendUtxoSelectionView: View { .padding(.bottom, 16) CustomButton(title: t("common__continue"), isDisabled: selectedUtxos.isEmpty || totalSelectedSats < totalRequiredSats) { - do { - wallet.selectedUtxos = wallet.availableUtxos.filter { selectedUtxos.contains($0.outpoint.txid) } - - navigationPath.append(.confirm) - } catch { - Logger.error(error, context: "Failed to set fee rate") - app.toast(type: .error, title: "Send Error", description: error.localizedDescription) - } + wallet.selectedUtxos = wallet.availableUtxos.filter { selectedUtxos.contains($0.outpoint.txid) } + navigationPath.append(.confirm) } .padding(.bottom, 16) } diff --git a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift index f058ae6cc..c47e6555e 100644 --- a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift +++ b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift @@ -186,7 +186,7 @@ struct BoostSheet: View { }) { HStack(spacing: 8) { VStack(alignment: .trailing, spacing: 2) { - if let feeRate { + if feeRate != nil { HStack(spacing: 2) { BodySSBText("₿ \(estimatedFeeSats)") @@ -194,7 +194,7 @@ struct BoostSheet: View { Image("pencil") .resizable() .frame(width: 16, height: 16) - .foregroundColor(feeRate != nil ? .textPrimary : .gray) + .foregroundColor(.textPrimary) } } } else if fetchingFees { diff --git a/BitkitUITests/BitkitUITests.swift b/BitkitUITests/BitkitUITests.swift index eda868bca..d7786fb2e 100644 --- a/BitkitUITests/BitkitUITests.swift +++ b/BitkitUITests/BitkitUITests.swift @@ -24,11 +24,9 @@ final class BitkitUITests: XCTestCase { } func testLaunchPerformance() { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() } } }