From 6b92e8f9b5adc8e57b533dd4f88f43d7fdca8ffb Mon Sep 17 00:00:00 2001 From: Aniket-pd Date: Fri, 20 Feb 2026 00:40:26 +0530 Subject: [PATCH 1/3] Add weekly background Kyoto CBF sync task (#352) --- .../App/BDKSwiftExampleWalletApp.swift | 78 +++++++++++++++++++ BDKSwiftExampleWallet/Info.plist | 8 ++ .../Service/Key Service/KeyService.swift | 2 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift b/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift index 7d7520ac..ed32229c 100644 --- a/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift +++ b/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift @@ -6,6 +6,7 @@ // import BitcoinDevKit +import BackgroundTasks import SwiftUI @main @@ -13,6 +14,12 @@ struct BDKSwiftExampleWalletApp: App { @AppStorage("isOnboarding") var isOnboarding: Bool = true @State private var navigationPath = NavigationPath() @State private var refreshTrigger = UUID() + @Environment(\.scenePhase) private var scenePhase + + init() { + BackgroundCBFSyncTask.register() + BackgroundCBFSyncTask.schedule() + } var body: some Scene { WindowGroup { @@ -30,6 +37,11 @@ struct BDKSwiftExampleWalletApp: App { BDKClient.live.setNeedsFullScan(true) navigationPath = NavigationPath() } + .onChange(of: scenePhase) { _, newValue in + if newValue == .background { + BackgroundCBFSyncTask.schedule() + } + } } } } @@ -42,3 +54,69 @@ extension BDKSwiftExampleWalletApp { return (try? KeyClient.live.getBackupInfo()) != nil } } + +private enum BackgroundCBFSyncTask { + static let identifier = "com.bitcoindevkit.bdkswiftexamplewallet.cbf-sync" + private static let minimumInterval: TimeInterval = 60 * 60 * 24 * 7 + + static func register() { + BGTaskScheduler.shared.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + guard let processingTask = task as? BGProcessingTask else { + task.setTaskCompleted(success: false) + return + } + handle(processingTask) + } + } + + static func schedule() { + let request = BGProcessingTaskRequest(identifier: identifier) + request.requiresNetworkConnectivity = true + request.requiresExternalPower = true + request.earliestBeginDate = Date(timeIntervalSinceNow: minimumInterval) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + print("[BackgroundCBF] Failed to schedule task: \(error)") + } + } + + private static func handle(_ task: BGProcessingTask) { + schedule() + + let syncTask = Task.detached(priority: .background) { + do { + try await runKyotoSync() + task.setTaskCompleted(success: true) + } catch { + print("[BackgroundCBF] Background sync failed: \(error)") + task.setTaskCompleted(success: false) + } + } + + task.expirationHandler = { + syncTask.cancel() + } + } + + private static func runKyotoSync() async throws { + let bdkClient = BDKClient.live + + guard (try? bdkClient.getBackupInfo()) != nil else { + return + } + + try bdkClient.loadWallet() + + guard bdkClient.getClientType() == .kyoto else { + return + } + + let inspector = WalletSyncScriptInspector(updateProgress: { _, _ in }) + try await bdkClient.syncWithInspector(inspector) + } +} diff --git a/BDKSwiftExampleWallet/Info.plist b/BDKSwiftExampleWallet/Info.plist index 990bd2ef..bd7bfd67 100644 --- a/BDKSwiftExampleWallet/Info.plist +++ b/BDKSwiftExampleWallet/Info.plist @@ -2,9 +2,17 @@ + BGTaskSchedulerPermittedIdentifiers + + com.bitcoindevkit.bdkswiftexamplewallet.cbf-sync + ITSAppUsesNonExemptEncryption NSPasteboardUsageDescription "To allow users to copy and paste text between the app and other apps" + UIBackgroundModes + + processing + diff --git a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift index 4ba5b546..02b0b03b 100644 --- a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift +++ b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift @@ -16,7 +16,7 @@ private struct KeyService { let keychain = Keychain(service: "com.matthewramsden.bdkswiftexamplewallet.testservice") .label(Bundle.main.displayName) .synchronizable(false) - .accessibility(.whenUnlocked) + .accessibility(.afterFirstUnlock) self.keychain = keychain } From b5affe3c3e990eabbbc094a8741bb35d93e5f5c1 Mon Sep 17 00:00:00 2001 From: Aniket-pd Date: Mon, 2 Mar 2026 20:29:55 +0530 Subject: [PATCH 2/3] version update to 2.3.1 --- BDKSwiftExampleWallet.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 5b93aa76..3c22d322 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -1071,7 +1071,7 @@ repositoryURL = "https://github.com/bitcoindevkit/bdk-swift"; requirement = { kind = exactVersion; - version = 2.3.0; + version = 2.3.1; }; }; AF77B2222F4B2C9E00000002 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { From 1dfe8d787d8bb314c449e7152d16ee271836675c Mon Sep 17 00:00:00 2001 From: Aniket-pd Date: Thu, 5 Mar 2026 02:58:10 +0530 Subject: [PATCH 3/3] Handle BG task expiration with single completion guard --- .../App/BDKSwiftExampleWalletApp.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift b/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift index ed32229c..18bcbcd7 100644 --- a/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift +++ b/BDKSwiftExampleWallet/App/BDKSwiftExampleWalletApp.swift @@ -59,6 +59,16 @@ private enum BackgroundCBFSyncTask { static let identifier = "com.bitcoindevkit.bdkswiftexamplewallet.cbf-sync" private static let minimumInterval: TimeInterval = 60 * 60 * 24 * 7 + private actor TaskCompletionTracker { + private var didComplete = false + + func complete(task: BGProcessingTask, success: Bool) { + guard !didComplete else { return } + didComplete = true + task.setTaskCompleted(success: success) + } + } + static func register() { BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, @@ -87,19 +97,23 @@ private enum BackgroundCBFSyncTask { private static func handle(_ task: BGProcessingTask) { schedule() + let completionTracker = TaskCompletionTracker() let syncTask = Task.detached(priority: .background) { do { try await runKyotoSync() - task.setTaskCompleted(success: true) + await completionTracker.complete(task: task, success: true) } catch { print("[BackgroundCBF] Background sync failed: \(error)") - task.setTaskCompleted(success: false) + await completionTracker.complete(task: task, success: false) } } task.expirationHandler = { syncTask.cancel() + Task { + await completionTracker.complete(task: task, success: false) + } } }