diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index cbdbb562b..c4dcb20da 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */; }; 6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */; }; 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; }; + 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */; }; 6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; }; 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; }; 65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */; }; @@ -510,6 +511,7 @@ 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = ""; }; 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = ""; }; 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = ""; }; + 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogNoticeView.swift; sourceTree = ""; }; 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = ""; }; 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = ""; }; 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsSettingsView.swift; sourceTree = ""; }; @@ -976,6 +978,7 @@ 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */, 657F98172F043D8100F732BD /* HomeContentView.swift */, 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */, + 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */, 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */, 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */, 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */, @@ -2264,6 +2267,7 @@ 65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */, 657F98182F043D8100F732BD /* HomeContentView.swift in Sources */, 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */, + 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */, DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */, DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */, DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */, diff --git a/LoopFollow/Settings/ShareLogNoticeView.swift b/LoopFollow/Settings/ShareLogNoticeView.swift new file mode 100644 index 000000000..5b6b3fe3b --- /dev/null +++ b/LoopFollow/Settings/ShareLogNoticeView.swift @@ -0,0 +1,37 @@ +// LoopFollow +// ShareLogNoticeView.swift + +import SwiftUI + +struct ShareLogNoticeView: View { + @State private var noticeText: String = "" + let onCancel: () -> Void + let onShare: (String) -> Void + + var body: some View { + NavigationView { + Form { + Section { + Text("Thanks for sharing these logs to help us find the problem. Please describe it in as much detail as possible — what time did it happen, what did you do, and what did you expect to happen that didn't?") + .font(.callout) + .foregroundColor(.secondary) + } + + Section(header: Text("Description")) { + TextEditor(text: $noticeText) + .frame(minHeight: 180) + } + } + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + .navigationBarTitle("Share Logs", displayMode: .inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", action: onCancel) + } + ToolbarItem(placement: .confirmationAction) { + Button("Share") { onShare(noticeText) } + } + } + } + } +} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index d74fc0625..b2274675d 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -60,6 +60,13 @@ extension Storage { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: legacyNotificationIDs) } + func migrateStep9() { + // Default for debugLogLevel changed from false to true so users ship useful + // logs when they report a problem. Force-enable for existing users. + LogManager.shared.log(category: .general, message: "Running migrateStep9 — enabling debug log level") + debugLogLevel.value = true + } + func migrateStep6() { // APNs credential separation LogManager.shared.log(category: .general, message: "Running migrateStep6 — APNs credential separation") diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index b9088ad0b..2dff9753f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -39,7 +39,7 @@ class Storage { var selectedBLEDevice = StorageValue(key: "selectedBLEDevice", defaultValue: nil) - var debugLogLevel = StorageValue(key: "debugLogLevel", defaultValue: false) + var debugLogLevel = StorageValue(key: "debugLogLevel", defaultValue: true) var contactTrend = StorageValue(key: "contactTrend", defaultValue: .off) var contactDelta = StorageValue(key: "contactDelta", defaultValue: .off) @@ -210,7 +210,7 @@ class Storage { // When adding a new migration step in `runMigrationsIfNeeded()`, bump this default // to the new latest step number so fresh installs skip all migrations. Other defaults // in this file must reflect the post-migration final state for a fresh install. - var migrationStep = StorageValue(key: "migrationStep", defaultValue: 7) + var migrationStep = StorageValue(key: "migrationStep", defaultValue: 9) var persistentNotification = StorageValue(key: "persistentNotification", defaultValue: false) var persistentNotificationLastBGTime = StorageValue(key: "persistentNotificationLastBGTime", defaultValue: .distantPast) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 6abea5ab4..b563855e7 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1082,6 +1082,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.migrateStep8() Storage.shared.migrationStep.value = 8 } + + if Storage.shared.migrationStep.value < 9 { + Storage.shared.migrateStep9() + Storage.shared.migrationStep.value = 9 + } } @objc func appDidBecomeActive() { diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift index 2693cde33..bbd24292e 100644 --- a/LoopFollow/ViewControllers/MoreMenuViewController.swift +++ b/LoopFollow/ViewControllers/MoreMenuViewController.swift @@ -343,10 +343,64 @@ class MoreMenuViewController: UIViewController { presentSimpleAlert(title: "No Logs Available", message: "There are no logs to share.") return } - let avc = UIActivityViewController(activityItems: files, applicationActivities: nil) + + let noticeView = ShareLogNoticeView( + onCancel: { [weak self] in + self?.dismiss(animated: true) + }, + onShare: { [weak self] noticeText in + self?.dismiss(animated: true) { + self?.presentLogShareSheet(noticeText: noticeText, logFiles: files) + } + } + ) + let host = UIHostingController(rootView: noticeView) + host.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + host.modalPresentationStyle = .formSheet + present(host, animated: true) + } + + private func presentLogShareSheet(noticeText: String, logFiles: [URL]) { + var items: [Any] = logFiles + if let noticeURL = writeShareNoticeFile(text: noticeText) { + items.insert(noticeURL, at: 0) + } + let avc = UIActivityViewController(activityItems: items, applicationActivities: nil) present(avc, animated: true) } + private func writeShareNoticeFile(text: String) -> URL? { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd_HHmm" + let timestamp = formatter.string(from: Date()) + + let version = AppVersionManager().version() + let branchAndSha = BuildDetails.default.branchAndSha + + let body = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? "(no description provided)" + : text + + let contents = """ + LoopFollow Log Share Notice + Date: \(ISO8601DateFormatter().string(from: Date())) + App version: \(version) (\(branchAndSha)) + + User description: + \(body) + """ + + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("ShareNotice_\(timestamp).txt") + do { + try contents.write(to: url, atomically: true, encoding: .utf8) + return url + } catch { + LogManager.shared.log(category: .general, message: "Failed to write share notice file: \(error)") + return nil + } + } + private func openURL(_ urlString: String) { if let url = URL(string: urlString) { UIApplication.shared.open(url)