Add StorageKeys, LegacyMigrationSourceKey, Value, ModernMigrationDestinationKey (+4 more)
This commit is contained in:
parent
f6d0760b20
commit
d7fe93bcf1
@ -45,6 +45,13 @@ struct ContentView: View {
|
|||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Sync", systemImage: "arrow.triangle.2.circlepath")
|
Label("Sync", systemImage: "arrow.triangle.2.circlepath")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
MigrationDemo()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Migration", systemImage: "sparkles")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,9 @@ nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
|||||||
.key(StorageKeys.SyncableSettingKey()),
|
.key(StorageKeys.SyncableSettingKey()),
|
||||||
.key(StorageKeys.ExternalKeyMaterialKey()),
|
.key(StorageKeys.ExternalKeyMaterialKey()),
|
||||||
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
||||||
.key(StorageKeys.AppGroupUserProfileKey())
|
.key(StorageKeys.AppGroupUserProfileKey()),
|
||||||
|
.key(StorageKeys.LegacyMigrationSourceKey()),
|
||||||
|
.key(StorageKeys.ModernMigrationDestinationKey())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
extension StorageKeys {
|
||||||
|
/// The legacy key where data starts (in UserDefaults)
|
||||||
|
struct LegacyMigrationSourceKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
|
||||||
|
let name = "legacy_user_id"
|
||||||
|
let domain: StorageDomain = .userDefaults(suite: nil)
|
||||||
|
let security: SecurityPolicy = .none
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Legacy key in UserDefaults from an older version of the app."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The modern key where data should end up (in Keychain)
|
||||||
|
struct ModernMigrationDestinationKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
|
||||||
|
let name = "secure_user_id"
|
||||||
|
let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage")
|
||||||
|
let security: SecurityPolicy = .keychain(
|
||||||
|
accessibility: .afterFirstUnlock,
|
||||||
|
accessControl: .userPresence
|
||||||
|
)
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Modern key in Keychain with biometric security."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
|
||||||
|
// Define the migration path
|
||||||
|
var migrationSources: [AnyStorageKey] {
|
||||||
|
[AnyStorageKey(LegacyMigrationSourceKey())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
SecureStorageSample/Views/MigrationDemo.swift
Normal file
117
SecureStorageSample/Views/MigrationDemo.swift
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct MigrationDemo: View {
|
||||||
|
@State private var legacyValue = ""
|
||||||
|
@State private var modernValue = ""
|
||||||
|
@State private var statusMessage = ""
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("The Scenario") {
|
||||||
|
Text("Imagine you have an old version of the app that stored a User ID in plain UserDefaults. Now, you want to move it to the secure Keychain automatically without the user noticing.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 1: Setup Legacy Data") {
|
||||||
|
Text("First, save a value to the 'legacy' key in UserDefaults.")
|
||||||
|
.font(.caption)
|
||||||
|
|
||||||
|
TextField("Legacy Value", text: $legacyValue)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
|
Button(action: saveToLegacy) {
|
||||||
|
Label("Save to Legacy (UserDefaults)", systemImage: "arrow.down.doc")
|
||||||
|
}
|
||||||
|
.disabled(legacyValue.isEmpty || isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 2: Trigger Migration") {
|
||||||
|
Text("Now, attempt to load from the 'modern' Keychain key. It will automatically check the legacy key, move the data, and delete the old record.")
|
||||||
|
.font(.caption)
|
||||||
|
|
||||||
|
Button(action: loadFromModern) {
|
||||||
|
Label("Load from Modern (Keychain)", systemImage: "sparkles")
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
|
||||||
|
if !modernValue.isEmpty {
|
||||||
|
LabeledContent("Migrated Value", value: modernValue)
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 3: Verify Cleanup") {
|
||||||
|
Text("Check if the data was actually removed from UserDefaults after migration.")
|
||||||
|
.font(.caption)
|
||||||
|
|
||||||
|
Button(action: checkLegacyExists) {
|
||||||
|
Label("Check Legacy Exists?", systemImage: "magnifyingglass")
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !statusMessage.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(statusMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(statusMessage.contains("Error") ? .red : .blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Data Migration")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
private func saveToLegacy() {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.LegacyMigrationSourceKey()
|
||||||
|
try await StorageRouter.shared.set(legacyValue, for: key)
|
||||||
|
statusMessage = "✓ Saved '\(legacyValue)' to legacy UserDefaults"
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadFromModern() {
|
||||||
|
isLoading = true
|
||||||
|
statusMessage = "Retrieving from Modern..."
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.ModernMigrationDestinationKey()
|
||||||
|
let value = try await StorageRouter.shared.get(key)
|
||||||
|
modernValue = value
|
||||||
|
statusMessage = "✓ Success! Data migrated from UserDefaults to Keychain."
|
||||||
|
} catch StorageError.notFound {
|
||||||
|
statusMessage = "Modern key is empty (and no legacy data found)."
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkLegacyExists() {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.LegacyMigrationSourceKey()
|
||||||
|
let exists = try await StorageRouter.shared.exists(key)
|
||||||
|
statusMessage = exists ? "⚠️ Legacy data STILL EXISTS in UserDefaults!" : "✅ Legacy data was successfully DELETED."
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user