Add StorageKeys, LegacyMigrationSourceKey, Value, ModernMigrationDestinationKey (+4 more)
This commit is contained in:
parent
f6d0760b20
commit
d7fe93bcf1
@ -45,6 +45,13 @@ struct ContentView: View {
|
||||
.tabItem {
|
||||
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.ExternalKeyMaterialKey()),
|
||||
.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