From 0db85ddc494ee1a7210a7be19ff67edae83ae58f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 16 Jan 2026 13:48:06 -0600 Subject: [PATCH] Add DemoDestination, AppStorageCatalog, StorageKeys, LegacyNotificationSettingKey (+14 more) --- SecureStorageSample/ContentView.swift | 89 ++++++++------- SecureStorageSample/Models/ProfileName.swift | 6 + .../Models/UnifiedSettings.swift | 6 + .../SecureStorageSampleApp.swift | 4 +- .../Services/AppStorageCatalog.swift | 13 ++- .../AppGroup/AppGroupUserDefaultsKey.swift | 2 +- .../AppGroup/AppGroupUserProfileKey.swift | 2 +- .../Migration/AggregatingMigrationKeys.swift | 73 +++++++++++++ .../Migration/ConditionalMigrationKeys.swift | 49 +++++++++ .../Migration/TransformingMigrationKeys.swift | 48 ++++++++ .../Views/AggregatingMigrationDemo.swift | 103 ++++++++++++++++++ .../Views/ConditionalMigrationDemo.swift | 93 ++++++++++++++++ .../Views/FileSystemDemo.swift | 2 + .../Views/MigrationHubView.swift | 39 +++++++ .../Views/TransformingMigrationDemo.swift | 102 +++++++++++++++++ 15 files changed, 584 insertions(+), 47 deletions(-) create mode 100644 SecureStorageSample/Models/ProfileName.swift create mode 100644 SecureStorageSample/Models/UnifiedSettings.swift create mode 100644 SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift create mode 100644 SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift create mode 100644 SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift create mode 100644 SecureStorageSample/Views/AggregatingMigrationDemo.swift create mode 100644 SecureStorageSample/Views/ConditionalMigrationDemo.swift create mode 100644 SecureStorageSample/Views/MigrationHubView.swift create mode 100644 SecureStorageSample/Views/TransformingMigrationDemo.swift diff --git a/SecureStorageSample/ContentView.swift b/SecureStorageSample/ContentView.swift index 5fed85d..0c21cd2 100644 --- a/SecureStorageSample/ContentView.swift +++ b/SecureStorageSample/ContentView.swift @@ -10,47 +10,47 @@ import LocalData struct ContentView: View { var body: some View { - TabView { - NavigationStack { - UserDefaultsDemo() + NavigationStack { + List { + Section("Storage Demos") { + NavigationLink(value: DemoDestination.userDefaults) { + Label("UserDefaults", systemImage: "gearshape.fill") + } + NavigationLink(value: DemoDestination.keychain) { + Label("Keychain", systemImage: "lock.fill") + } + NavigationLink(value: DemoDestination.files) { + Label("File Storage", systemImage: "doc.fill") + } + NavigationLink(value: DemoDestination.encrypted) { + Label("Encrypted Storage", systemImage: "lock.shield.fill") + } + NavigationLink(value: DemoDestination.sync) { + Label("Watch Sync", systemImage: "arrow.triangle.2.circlepath") + } + NavigationLink(value: DemoDestination.migration) { + Label("Migrations", systemImage: "sparkles") + } + } } - .tabItem { - Label("Defaults", systemImage: "gearshape.fill") - } - - NavigationStack { - KeychainDemo() - } - .tabItem { - Label("Keychain", systemImage: "lock.fill") - } - - NavigationStack { - FileSystemDemo() - } - .tabItem { - Label("Files", systemImage: "doc.fill") - } - - NavigationStack { - EncryptedStorageDemo() - } - .tabItem { - Label("Encrypted", systemImage: "lock.shield.fill") - } - - NavigationStack { - PlatformSyncDemo() - } - .tabItem { - Label("Sync", systemImage: "arrow.triangle.2.circlepath") - } - - NavigationStack { - MigrationDemo() - } - .tabItem { - Label("Migration", systemImage: "sparkles") + .scrollIndicators(.hidden) + .navigationTitle("Secure Storage") + .navigationBarTitleDisplayMode(.inline) + .navigationDestination(for: DemoDestination.self) { destination in + switch destination { + case .userDefaults: + UserDefaultsDemo() + case .keychain: + KeychainDemo() + case .files: + FileSystemDemo() + case .encrypted: + EncryptedStorageDemo() + case .sync: + PlatformSyncDemo() + case .migration: + MigrationHubView() + } } } } @@ -59,3 +59,12 @@ struct ContentView: View { #Preview { ContentView() } + +private enum DemoDestination: Hashable { + case userDefaults + case keychain + case files + case encrypted + case sync + case migration +} diff --git a/SecureStorageSample/Models/ProfileName.swift b/SecureStorageSample/Models/ProfileName.swift new file mode 100644 index 0000000..3a33ba9 --- /dev/null +++ b/SecureStorageSample/Models/ProfileName.swift @@ -0,0 +1,6 @@ +import Foundation + +nonisolated struct ProfileName: Codable, Sendable { + let firstName: String + let lastName: String +} diff --git a/SecureStorageSample/Models/UnifiedSettings.swift b/SecureStorageSample/Models/UnifiedSettings.swift new file mode 100644 index 0000000..4206b8c --- /dev/null +++ b/SecureStorageSample/Models/UnifiedSettings.swift @@ -0,0 +1,6 @@ +import Foundation + +nonisolated struct UnifiedSettings: Codable, Sendable { + let notificationsEnabled: Bool + let theme: String +} diff --git a/SecureStorageSample/SecureStorageSampleApp.swift b/SecureStorageSample/SecureStorageSampleApp.swift index 679a9dd..ba98db9 100644 --- a/SecureStorageSample/SecureStorageSampleApp.swift +++ b/SecureStorageSample/SecureStorageSampleApp.swift @@ -48,7 +48,7 @@ struct SecureStorageSampleApp: App { await StorageRouter.shared.updateStorageConfiguration(storageConfig) do { - try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self, migrateImmediately: true) + try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true) } catch { assertionFailure("Storage catalog registration failed: \(error)") } @@ -58,7 +58,7 @@ struct SecureStorageSampleApp: App { ) } #if DEBUG - let report = StorageAuditReport.renderText(for: AppStorageCatalog.self) + let report = StorageAuditReport.renderText(AppStorageCatalog()) print(report) #endif } diff --git a/SecureStorageSample/Services/AppStorageCatalog.swift b/SecureStorageSample/Services/AppStorageCatalog.swift index f9b4c78..b0ddab9 100644 --- a/SecureStorageSample/Services/AppStorageCatalog.swift +++ b/SecureStorageSample/Services/AppStorageCatalog.swift @@ -3,8 +3,8 @@ import LocalData import SharedKit -nonisolated struct AppStorageCatalog: StorageKeyCatalog { - static var allKeys: [AnyStorageKey] { +struct AppStorageCatalog: StorageKeyCatalog { + var allKeys: [AnyStorageKey] { [ .key(StorageKeys.AppVersionKey()), .key(StorageKeys.UserPreferencesKey()), @@ -23,7 +23,14 @@ nonisolated struct AppStorageCatalog: StorageKeyCatalog { .key(StorageKeys.AppGroupUserDefaultsKey()), .key(StorageKeys.AppGroupUserProfileKey()), .key(StorageKeys.LegacyMigrationSourceKey()), - .key(StorageKeys.ModernMigrationDestinationKey()) + .key(StorageKeys.ModernMigrationDestinationKey()), + .key(StorageKeys.LegacyProfileNameKey()), + .key(StorageKeys.ModernProfileNameKey()), + .key(StorageKeys.LegacyNotificationSettingKey()), + .key(StorageKeys.LegacyThemeSettingKey()), + .key(StorageKeys.ModernUnifiedSettingsKey()), + .key(StorageKeys.LegacyAppModeKey()), + .key(StorageKeys.ModernAppModeKey()) ] } } diff --git a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift index 93e0a13..9d51552 100644 --- a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift +++ b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift @@ -15,7 +15,7 @@ extension StorageKeys { let serializer: Serializer = .json let owner = "SampleApp" let description = "Stores a shared setting readable by app extensions." - let availability: PlatformAvailability = .all + let availability: PlatformAvailability = .phoneOnly let syncPolicy: SyncPolicy = .never } } diff --git a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift index ebca327..985b56b 100644 --- a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift +++ b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift @@ -16,7 +16,7 @@ extension StorageKeys { let serializer: Serializer = .json let owner = "SampleApp" let description = "Stores a profile shared between the app and extensions." - let availability: PlatformAvailability = .all + let availability: PlatformAvailability = .phoneOnly let syncPolicy: SyncPolicy = .never init(directory: FileDirectory = .documents) { diff --git a/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift new file mode 100644 index 0000000..5ee521d --- /dev/null +++ b/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift @@ -0,0 +1,73 @@ +import Foundation +import LocalData + +extension StorageKeys { + struct LegacyNotificationSettingKey: StorageKey { + typealias Value = Bool + + let name = "legacy_notification_setting" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Legacy notification setting stored as Bool." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + } + + struct LegacyThemeSettingKey: StorageKey { + typealias Value = String + + let name = "legacy_theme_setting" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Legacy theme setting stored as a string." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + } + + struct ModernUnifiedSettingsKey: StorageKey { + typealias Value = UnifiedSettings + + let name = "modern_unified_settings" + let domain: StorageDomain = .fileSystem(directory: .documents) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Modern unified settings aggregated from legacy keys." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + + var migration: AnyStorageMigration? { + let sources: [AnyStorageKey] = [ + .key(StorageKeys.LegacyNotificationSettingKey()), + .key(StorageKeys.LegacyThemeSettingKey()) + ] + + return AnyStorageMigration( + DefaultAggregatingMigration( + destinationKey: self, + sourceKeys: sources + ) { sources in + var notificationsEnabled = false + var theme = "system" + + for source in sources { + if let boolValue = source.value as? Bool { + notificationsEnabled = boolValue + } else if let stringValue = source.value as? String { + theme = stringValue + } + } + + return UnifiedSettings( + notificationsEnabled: notificationsEnabled, + theme: theme + ) + } + ) + } + } +} diff --git a/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift new file mode 100644 index 0000000..b0d6f04 --- /dev/null +++ b/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift @@ -0,0 +1,49 @@ +import Foundation +import LocalData + +extension StorageKeys { + struct LegacyAppModeKey: StorageKey { + typealias Value = String + + let name = "legacy_app_mode" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Legacy app mode stored in UserDefaults." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + } + + struct ModernAppModeKey: StorageKey { + typealias Value = String + + let name = "modern_app_mode" + let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage") + let security: SecurityPolicy = .keychain( + accessibility: .afterFirstUnlock, + accessControl: .userPresence + ) + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Modern app mode with conditional migration." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + + var migration: AnyStorageMigration? { + let destination = self + let legacy = StorageKeys.LegacyAppModeKey() + let fallback = AnyStorageMigration( + SimpleLegacyMigration(destinationKey: destination, sourceKey: .key(legacy)) + ) + + return AnyStorageMigration( + AppVersionConditionalMigration( + destinationKey: destination, + minAppVersion: "99.0", + fallbackMigration: fallback + ) + ) + } + } +} diff --git a/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift new file mode 100644 index 0000000..0402b53 --- /dev/null +++ b/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift @@ -0,0 +1,48 @@ +import Foundation +import LocalData + +extension StorageKeys { + struct LegacyProfileNameKey: StorageKey { + typealias Value = String + + let name = "legacy_profile_name" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Legacy profile name stored as a single string." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + } + + struct ModernProfileNameKey: StorageKey { + typealias Value = ProfileName + + let name = "modern_profile_name" + let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage") + let security: SecurityPolicy = .keychain( + accessibility: .afterFirstUnlock, + accessControl: .userPresence + ) + let serializer: Serializer = .json + let owner = "MigrationDemo" + let description = "Modern profile name stored as structured data." + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .never + + var migration: AnyStorageMigration? { + let sourceKey = StorageKeys.LegacyProfileNameKey() + return AnyStorageMigration( + DefaultTransformingMigration( + destinationKey: self, + sourceKey: sourceKey + ) { value in + let parts = value.split(separator: " ", maxSplits: 1).map(String.init) + let firstName = parts.first ?? "" + let lastName = parts.count > 1 ? parts[1] : "" + return ProfileName(firstName: firstName, lastName: lastName) + } + ) + } + } +} diff --git a/SecureStorageSample/Views/AggregatingMigrationDemo.swift b/SecureStorageSample/Views/AggregatingMigrationDemo.swift new file mode 100644 index 0000000..7eb45b5 --- /dev/null +++ b/SecureStorageSample/Views/AggregatingMigrationDemo.swift @@ -0,0 +1,103 @@ +import SwiftUI +import LocalData + +@MainActor +struct AggregatingMigrationDemo: View { + @State private var notificationsEnabled = false + @State private var theme = "" + @State private var modernValue = "" + @State private var statusMessage = "" + @State private var isLoading = false + + var body: some View { + Form { + Section("The Scenario") { + Text("Two legacy settings are aggregated into a single unified settings model during migration.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + } + + Section("Step 1: Setup Legacy Data") { + Toggle("Notifications Enabled", isOn: $notificationsEnabled) + + TextField("Theme", text: $theme) + .textFieldStyle(.roundedBorder) + + Button(action: saveToLegacy) { + Label("Save Legacy Settings", systemImage: "tray.and.arrow.down") + } + .disabled(theme.isEmpty || isLoading) + } + + Section("Step 2: Trigger Migration") { + Text("Load the modern key to aggregate legacy values into unified settings.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + + Button(action: loadFromModern) { + Label("Load Unified Settings", systemImage: "sparkles") + } + .disabled(isLoading) + + if !modernValue.isEmpty { + LabeledContent("Unified Settings", value: modernValue) + .foregroundStyle(Color.Status.success) + .bold() + } + } + + if !statusMessage.isEmpty { + Section { + Text(statusMessage) + .font(.caption) + .foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info) + } + } + } + .navigationTitle("Aggregating Migration") + .navigationBarTitleDisplayMode(.inline) + } + + private func saveToLegacy() { + isLoading = true + Task { + do { + try await StorageRouter.shared.set( + notificationsEnabled, + for: StorageKeys.LegacyNotificationSettingKey() + ) + try await StorageRouter.shared.set( + theme, + for: StorageKeys.LegacyThemeSettingKey() + ) + statusMessage = "✓ Saved legacy settings" + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } + + private func loadFromModern() { + isLoading = true + statusMessage = "Loading unified settings..." + Task { + do { + let value = try await StorageRouter.shared.get(StorageKeys.ModernUnifiedSettingsKey()) + modernValue = format(value) + statusMessage = "✓ Migration complete." + } catch StorageError.notFound { + statusMessage = "Modern key is empty (and no legacy data found)." + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } + + private func format(_ value: UnifiedSettings) -> String { + let notificationsText = value.notificationsEnabled ? "On" : "Off" + let themeText = value.theme.isEmpty ? "system" : value.theme + return "Notifications: \(notificationsText), Theme: \(themeText)" + } +} diff --git a/SecureStorageSample/Views/ConditionalMigrationDemo.swift b/SecureStorageSample/Views/ConditionalMigrationDemo.swift new file mode 100644 index 0000000..f454909 --- /dev/null +++ b/SecureStorageSample/Views/ConditionalMigrationDemo.swift @@ -0,0 +1,93 @@ +import SwiftUI +import LocalData + +@MainActor +struct ConditionalMigrationDemo: 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("A conditional migration only runs when the app version is below a threshold.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + } + + Section("Step 1: Setup Legacy Data") { + Text("Save a legacy app mode string that will migrate only if the version condition is met.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + + TextField("Legacy App Mode", text: $legacyValue) + .textFieldStyle(.roundedBorder) + + Button(action: saveToLegacy) { + Label("Save Legacy Mode", systemImage: "square.and.arrow.down") + } + .disabled(legacyValue.isEmpty || isLoading) + } + + Section("Step 2: Trigger Conditional Migration") { + Text("Load the modern key to run the conditional migration.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + + Button(action: loadFromModern) { + Label("Load Modern Mode", systemImage: "sparkles") + } + .disabled(isLoading) + + if !modernValue.isEmpty { + LabeledContent("Migrated Mode", value: modernValue) + .foregroundStyle(Color.Status.success) + .bold() + } + } + + if !statusMessage.isEmpty { + Section { + Text(statusMessage) + .font(.caption) + .foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info) + } + } + } + .navigationTitle("Conditional Migration") + .navigationBarTitleDisplayMode(.inline) + } + + private func saveToLegacy() { + isLoading = true + Task { + do { + let key = StorageKeys.LegacyAppModeKey() + try await StorageRouter.shared.set(legacyValue, for: key) + statusMessage = "✓ Saved legacy app mode" + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } + + private func loadFromModern() { + isLoading = true + statusMessage = "Loading modern mode..." + Task { + do { + let key = StorageKeys.ModernAppModeKey() + let value = try await StorageRouter.shared.get(key) + modernValue = value + statusMessage = "✓ Conditional migration complete." + } catch StorageError.notFound { + statusMessage = "Modern key is empty (and no legacy data found)." + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } +} diff --git a/SecureStorageSample/Views/FileSystemDemo.swift b/SecureStorageSample/Views/FileSystemDemo.swift index ce9eb98..4f0266f 100644 --- a/SecureStorageSample/Views/FileSystemDemo.swift +++ b/SecureStorageSample/Views/FileSystemDemo.swift @@ -83,6 +83,7 @@ struct FileSystemDemo: View { .disabled(isLoading) } + #if os(iOS) Section("App Group Storage") { Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.") .font(.caption) @@ -113,6 +114,7 @@ struct FileSystemDemo: View { .foregroundStyle(.red) .disabled(isLoading) } + #endif if let profile = storedProfile { Section("Retrieved Profile") { diff --git a/SecureStorageSample/Views/MigrationHubView.swift b/SecureStorageSample/Views/MigrationHubView.swift new file mode 100644 index 0000000..a6aa566 --- /dev/null +++ b/SecureStorageSample/Views/MigrationHubView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct MigrationHubView: View { + var body: some View { + Form { + Section("Migration Demos") { + NavigationLink("Simple Legacy Migration") { + MigrationDemo() + } + + NavigationLink("Transforming Migration") { + TransformingMigrationDemo() + } + + NavigationLink("Aggregating Migration") { + AggregatingMigrationDemo() + } + + NavigationLink("Conditional Migration") { + ConditionalMigrationDemo() + } + } + + Section("Overview") { + Text("Explore migrations that move legacy values into modern storage with transformation, aggregation, and conditional rules.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + } + } + .navigationTitle("Migrations") + .navigationBarTitleDisplayMode(.inline) + } +} + +#Preview { + NavigationStack { + MigrationHubView() + } +} diff --git a/SecureStorageSample/Views/TransformingMigrationDemo.swift b/SecureStorageSample/Views/TransformingMigrationDemo.swift new file mode 100644 index 0000000..8a60bb7 --- /dev/null +++ b/SecureStorageSample/Views/TransformingMigrationDemo.swift @@ -0,0 +1,102 @@ +import SwiftUI +import LocalData + +@MainActor +struct TransformingMigrationDemo: 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("A legacy profile name stored as a single string is migrated into a structured model with first and last names.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + } + + Section("Step 1: Setup Legacy Data") { + Text("Save a full name into the legacy UserDefaults key.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + + TextField("Legacy Full Name", text: $legacyValue) + .textFieldStyle(.roundedBorder) + + Button(action: saveToLegacy) { + Label("Save Legacy Name", systemImage: "person.crop.circle.badge.plus") + } + .disabled(legacyValue.isEmpty || isLoading) + } + + Section("Step 2: Trigger Migration") { + Text("Load the modern key to transform the legacy value into a structured profile.") + .font(.caption) + .foregroundStyle(Color.Text.secondary) + + Button(action: loadFromModern) { + Label("Load Modern Profile", systemImage: "sparkles") + } + .disabled(isLoading) + + if !modernValue.isEmpty { + LabeledContent("Migrated Name", value: modernValue) + .foregroundStyle(Color.Status.success) + .bold() + } + } + + if !statusMessage.isEmpty { + Section { + Text(statusMessage) + .font(.caption) + .foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info) + } + } + } + .navigationTitle("Transforming Migration") + .navigationBarTitleDisplayMode(.inline) + } + + private func saveToLegacy() { + isLoading = true + Task { + do { + let key = StorageKeys.LegacyProfileNameKey() + try await StorageRouter.shared.set(legacyValue, for: key) + statusMessage = "✓ Saved legacy name \(legacyValue)" + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } + + private func loadFromModern() { + isLoading = true + statusMessage = "Loading modern profile..." + Task { + do { + let key = StorageKeys.ModernProfileNameKey() + let value = try await StorageRouter.shared.get(key) + modernValue = format(value) + statusMessage = "✓ Migration complete." + } catch StorageError.notFound { + statusMessage = "Modern key is empty (and no legacy data found)." + } catch { + statusMessage = "Error: \(error.localizedDescription)" + } + isLoading = false + } + } + + private func format(_ value: ProfileName) -> String { + let trimmedFirst = value.firstName.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedLast = value.lastName.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedLast.isEmpty { + return trimmedFirst + } + return "\(trimmedFirst) \(trimmedLast)" + } +}