From 8cd698d5c0ace70d655c466d882d46f0af00056b Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Wed, 8 Jul 2026 10:32:14 -0400 Subject: [PATCH] fix: migrate legacy backups missing wallet id on restore Backups written before activity data became wallet-scoped have no wallet id on their activity, tag and pre-activity-metadata records. With bitkit-core 0.4.0 those records require a wallet id, so decoding such a backup throws before the records reach Core. Before decoding a restored envelope, hand each Core-owned array to the matching bitkit-core migration helper (migrateBackupActivitiesJson, migrateBackupActivityTagsJson, migrateBackupPreActivityMetadataJson), which fills in the default wallet id for records that lack one and preserves any existing id. The app never edits Core model JSON itself, and current backups that already carry a wallet id are unaffected. Bumps bitkit-core to 0.4.0. --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- Bitkit/Services/BackupService.swift | 33 +++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index a1a39b9ac..5764cba30 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1201,7 +1201,7 @@ repositoryURL = "https://github.com/synonymdev/bitkit-core"; requirement = { kind = exactVersion; - version = 0.3.9; + version = 0.4.0; }; }; 96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a418ff0d1..3d786848f 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/synonymdev/bitkit-core", "state" : { - "revision" : "5e08294bc5701d8b48337745990a417841d6e026", - "version" : "0.3.9" + "revision" : "4979f4d23aaaeec11a09b77c8117a0afdaad4317", + "version" : "0.4.0" } }, { diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift index 5a8c0c386..a82138fd6 100644 --- a/Bitkit/Services/BackupService.swift +++ b/Bitkit/Services/BackupService.swift @@ -211,7 +211,11 @@ class BackupService { } try await performRestore(category: .activity) { dataBytes in - let payload = try JSONDecoder().decode(ActivityBackupV1.self, from: dataBytes) + let migrated = try self.migrateCoreOwnedBackupFields(dataBytes, fieldMigrations: [ + "activities": migrateBackupActivitiesJson, + "activityTags": migrateBackupActivityTagsJson, + ]) + let payload = try JSONDecoder().decode(ActivityBackupV1.self, from: migrated) try await CoreService.shared.activity.upsertList(payload.activities) try await CoreService.shared.activity.upsertTags(payload.activityTags) @@ -224,7 +228,10 @@ class BackupService { } try await performRestore(category: .metadata) { dataBytes in - let payload = try JSONDecoder().decode(MetadataBackupV1.self, from: dataBytes) + let migrated = try self.migrateCoreOwnedBackupFields(dataBytes, fieldMigrations: [ + "tagMetadata": migrateBackupPreActivityMetadataJson, + ]) + let payload = try JSONDecoder().decode(MetadataBackupV1.self, from: migrated) try await CoreService.shared.activity.upsertPreActivityMetadata(payload.tagMetadata) @@ -739,6 +746,28 @@ class BackupService { } } + /// Fills in wallet ids that predate wallet-scoped activity data before a + /// backup envelope is decoded. Each Core-owned array field is handed to the + /// matching Core migration helper as raw JSON, so the app never edits Core + /// model JSON itself. Records that already carry a wallet id are left + /// unchanged, so this is safe to run on current backups too. + private func migrateCoreOwnedBackupFields( + _ dataBytes: Data, + fieldMigrations: [String: (String) throws -> String] + ) throws -> Data { + guard var root = try JSONSerialization.jsonObject(with: dataBytes) as? [String: Any] else { + return dataBytes + } + for (field, migrate) in fieldMigrations { + guard let array = root[field] as? [Any] else { continue } + let arrayData = try JSONSerialization.data(withJSONObject: array) + let migratedJson = try migrate(String(decoding: arrayData, as: UTF8.self)) + guard let migratedData = migratedJson.data(using: .utf8) else { continue } + root[field] = try JSONSerialization.jsonObject(with: migratedData) + } + return try JSONSerialization.data(withJSONObject: root) + } + private func performRestore(category: BackupCategory, restoreAction: (Data) async throws -> Void) async throws { do { let item = try await vssBackupClient.getObject(key: category.rawValue)