From 48b1b5cd4a9f3112cf4927659dd32b4afa118c69 Mon Sep 17 00:00:00 2001 From: codebymini Date: Sun, 19 Jul 2026 21:52:25 +0200 Subject: [PATCH] Add new section to export APNS setting --- .../ImportExport/ExportableSettings.swift | 41 +++++++++++++++++ .../ImportExportSettingsView.swift | 18 +++++++- .../ImportExportSettingsViewModel.swift | 44 +++++++++++++++++-- .../SettingsMigrationManager.swift | 9 ++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/LoopFollow/Settings/ImportExport/ExportableSettings.swift b/LoopFollow/Settings/ImportExport/ExportableSettings.swift index cd222417c..3571d8cfb 100644 --- a/LoopFollow/Settings/ImportExport/ExportableSettings.swift +++ b/LoopFollow/Settings/ImportExport/ExportableSettings.swift @@ -389,6 +389,44 @@ struct RemoteSettingsExport: Codable { } } +// MARK: - APNS Settings Export + +struct APNSSettingsExport: Codable { + let version: String + let keyId: String + let apnsKey: String + + static func fromCurrentStorage() -> APNSSettingsExport { + let storage = Storage.shared + return APNSSettingsExport( + version: AppVersionManager().version(), + keyId: storage.lfKeyId.value, + apnsKey: storage.lfApnsKey.value + ) + } + + func applyToStorage() { + let storage = Storage.shared + storage.lfKeyId.value = keyId + + let apnsService = LoopAPNSService() + storage.lfApnsKey.value = apnsService.validateAndFixAPNSKey(apnsKey) + } + + func encodeToJSON() -> String? { + do { + let data = try JSONEncoder().encode(self) + return String(data: data, encoding: .utf8) + } catch { + return nil + } + } + + func hasValidSettings() -> Bool { + APNsCredentialValidator.isFullyConfigured(keyId: keyId, apnsKey: apnsKey) + } +} + // MARK: - Combined Settings Export struct CombinedSettingsExport: Codable { @@ -398,6 +436,7 @@ struct CombinedSettingsExport: Codable { let dexcom: DexcomSettingsExport? let remote: RemoteSettingsExport? let alarms: AlarmSettingsExport? + let apns: APNSSettingsExport? let exportType: String let timestamp: Date @@ -405,6 +444,7 @@ struct CombinedSettingsExport: Codable { dexcom: DexcomSettingsExport? = nil, remote: RemoteSettingsExport? = nil, alarms: AlarmSettingsExport? = nil, + apns: APNSSettingsExport? = nil, exportType: String) { version = "1.0" @@ -413,6 +453,7 @@ struct CombinedSettingsExport: Codable { self.dexcom = dexcom self.remote = remote self.alarms = alarms + self.apns = apns self.exportType = exportType timestamp = Date() } diff --git a/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift b/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift index 6082b2187..e7fc37770 100644 --- a/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift +++ b/LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift @@ -83,7 +83,7 @@ struct ImportExportSettingsView: View { ) .padding() - Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.rawValue.lowercased())") + Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.importDescription)") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) @@ -187,6 +187,15 @@ struct ImportConfirmationView: View { color: .red ) } + + if let apnsKeyId = preview.apnsKeyId, !apnsKeyId.isEmpty { + SettingRowView( + icon: "key.horizontal", + title: "APNS Key ID", + value: apnsKeyId, + color: .purple + ) + } } .padding(.horizontal) } @@ -206,6 +215,13 @@ struct ImportConfirmationView: View { .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) + + if let apnsKeyId = viewModel.importPreview?.apnsKeyId, !apnsKeyId.isEmpty { + Text("APNS warning: importing APNS settings will overwrite your existing APNS Key ID and APNS Key.") + .font(.caption) + .foregroundColor(.orange) + .multilineTextAlignment(.center) + } } .padding(.horizontal) diff --git a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift index cea659f85..0550e609e 100644 --- a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift +++ b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift @@ -10,6 +10,7 @@ struct ImportPreview { let remoteType: String? let alarmCount: Int let alarmNames: [String] + let apnsKeyId: String? } class ImportExportSettingsViewModel: ObservableObject { @@ -38,6 +39,7 @@ class ImportExportSettingsViewModel: ObservableObject { case nightscout = "Nightscout Settings" case dexcom = "Dexcom Share Settings" case remote = "Remote Settings" + case apns = "APNS Settings" case alarms = "Alarm Settings" var icon: String { @@ -46,6 +48,17 @@ class ImportExportSettingsViewModel: ObservableObject { case .dexcom: return "network" case .remote: return "antenna.radiowaves.left.and.right" case .alarms: return "bell" + case .apns: return "key.horizontal" + } + } + + var importDescription: String { + switch self { + case .nightscout: return "Nightscout Settings" + case .dexcom: return "Dexcom Share Settings" + case .remote: return "Remote Settings" + case .alarms: return "Alarm Settings" + case .apns: return "APNS Settings" } } } @@ -91,7 +104,7 @@ class ImportExportSettingsViewModel: ObservableObject { return } - LogManager.shared.log(category: .general, message: "QR code decoded successfully. Components: nightscout=\(settings.nightscout != nil), remote=\(settings.remote != nil), alarms=\(settings.alarms != nil)") + LogManager.shared.log(category: .general, message: "QR code decoded successfully. Components: nightscout=\(settings.nightscout != nil), remote=\(settings.remote != nil), alarms=\(settings.alarms != nil), apns=\(settings.apns != nil)") // Check version compatibility let currentVersion = AppVersionManager().version() @@ -163,6 +176,16 @@ class ImportExportSettingsViewModel: ObservableObject { LogManager.shared.log(category: .general, message: "Alarm settings imported from \(source) (version: \(alarms.version), \(alarms.alarms.count) alarms)") } + if let apns = settings.apns { + if apns.hasValidSettings() { + apns.applyToStorage() + importedComponents.append("APNS settings") + LogManager.shared.log(category: .general, message: "APNS settings imported from \(source) (version: \(apns.version), keyId: \(apns.keyId))") + } else { + throw NSError(domain: "SettingsImport", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid APNS settings in \(source)"]) + } + } + // Update the success message with what was imported if !importedComponents.isEmpty { let componentsList = importedComponents.joined(separator: ", ") @@ -220,6 +243,16 @@ class ImportExportSettingsViewModel: ObservableObject { dexcom: dexcomSettings, exportType: exportType.rawValue ) + case .apns: + let apnsSettings = APNSSettingsExport.fromCurrentStorage() + if !apnsSettings.hasValidSettings() { + qrCodeErrorMessage = "Please configure your APNS settings first (valid APNS Key ID and APNS Key)" + return nil + } + settings = CombinedSettingsExport( + apns: apnsSettings, + exportType: exportType.rawValue + ) } return settings?.encodeToJSON() @@ -257,14 +290,16 @@ class ImportExportSettingsViewModel: ObservableObject { let remoteType = settings.remote?.remoteType != .none ? settings.remote?.remoteType.rawValue : nil let alarmCount = settings.alarms?.alarms.count ?? 0 let alarmNames = settings.alarms?.alarms.map { $0.name } ?? [] + let apnsKeyId = settings.apns?.keyId.isEmpty == false ? settings.apns?.keyId : nil // Check if any settings are actually present let hasAnySettings = (nightscoutURL != nil && !nightscoutURL!.isEmpty) || (dexcomUsername != nil && !dexcomUsername!.isEmpty) || (remoteType != nil && !remoteType!.isEmpty && remoteType != "None") || - alarmCount > 0 + alarmCount > 0 || + (apnsKeyId != nil && !apnsKeyId!.isEmpty) - LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), hasAnySettings: \(hasAnySettings)") + LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), apnsKeyIdPresent: \(apnsKeyId != nil), hasAnySettings: \(hasAnySettings)") if hasAnySettings { LogManager.shared.log(category: .general, message: "Creating import preview with settings") @@ -273,7 +308,8 @@ class ImportExportSettingsViewModel: ObservableObject { dexcomUsername: dexcomUsername, remoteType: remoteType, alarmCount: alarmCount, - alarmNames: alarmNames + alarmNames: alarmNames, + apnsKeyId: apnsKeyId ) LogManager.shared.log(category: .general, message: "Created importPreview - nightscoutURL: \(importPreview?.nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), remoteType: \(importPreview?.remoteType ?? "nil"), alarmCount: \(importPreview?.alarmCount ?? 0)") showImportConfirmation = true diff --git a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift index c4e2a6b20..0c47f2dc9 100644 --- a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift +++ b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift @@ -61,6 +61,15 @@ class SettingsMigrationManager { ) } + // Try to decode as APNSSettingsExport + if let apnsSettings = try? JSONDecoder().decode(APNSSettingsExport.self, from: data) { + LogManager.shared.log(category: .general, message: "Successfully decoded as APNSSettingsExport", isDebug: true) + return CombinedSettingsExport( + apns: apnsSettings, + exportType: "APNS Settings" + ) + } + LogManager.shared.log(category: .general, message: "Failed to decode as any known component", isDebug: true) return nil }