Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LoopSupport.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
C12ECD06289560B90089EB07 /* LoopSupportKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A936A95425BC9A190085119C /* LoopSupportKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C1A55E022E9F000000B10002 /* IssueReportContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E022E9F000000B10001 /* IssueReportContext.swift */; };
C1A55E022E9F000000B10004 /* IssueReportSubmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E022E9F000000B10003 /* IssueReportSubmitter.swift */; };
C1A55E022E9F000000B1000A /* DiagnosticReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E022E9F000000B10009 /* DiagnosticReportView.swift */; };
C1A55E022E9F000000B10006 /* ReportProblemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E022E9F000000B10005 /* ReportProblemView.swift */; };
C1A55E022E9F000000B10008 /* SendIssueReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E022E9F000000B10007 /* SendIssueReportView.swift */; };
C14A7E7C295F2EC100CD87B4 /* UsageDataPrivacyPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14A7E7B295F2EC100CD87B4 /* UsageDataPrivacyPreferenceView.swift */; };
Expand Down Expand Up @@ -90,6 +91,7 @@
B66D1F542E6A752300471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
C1A55E022E9F000000B10001 /* IssueReportContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReportContext.swift; sourceTree = "<group>"; };
C1A55E022E9F000000B10003 /* IssueReportSubmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReportSubmitter.swift; sourceTree = "<group>"; };
C1A55E022E9F000000B10009 /* DiagnosticReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReportView.swift; sourceTree = "<group>"; };
C1A55E022E9F000000B10005 /* ReportProblemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProblemView.swift; sourceTree = "<group>"; };
C1A55E022E9F000000B10007 /* SendIssueReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIssueReportView.swift; sourceTree = "<group>"; };
C14A7E7B295F2EC100CD87B4 /* UsageDataPrivacyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsageDataPrivacyPreferenceView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -197,6 +199,7 @@
C1A55E022E9F000000B10000 /* IssueReport */ = {
isa = PBXGroup;
children = (
C1A55E022E9F000000B10009 /* DiagnosticReportView.swift */,
C1A55E022E9F000000B10001 /* IssueReportContext.swift */,
C1A55E022E9F000000B10003 /* IssueReportSubmitter.swift */,
C1A55E022E9F000000B10005 /* ReportProblemView.swift */,
Expand Down Expand Up @@ -371,6 +374,7 @@
C14A7E7C295F2EC100CD87B4 /* UsageDataPrivacyPreferenceView.swift in Sources */,
C1A55E022E9F000000B10002 /* IssueReportContext.swift in Sources */,
C1A55E022E9F000000B10004 /* IssueReportSubmitter.swift in Sources */,
C1A55E022E9F000000B1000A /* DiagnosticReportView.swift in Sources */,
C1A55E022E9F000000B10006 /* ReportProblemView.swift in Sources */,
C1A55E022E9F000000B10008 /* SendIssueReportView.swift in Sources */,
A9CE7D8425BC9B3700C04F9B /* LocalizedString.swift in Sources */,
Expand Down
100 changes: 100 additions & 0 deletions LoopSupportKitUI/IssueReport/DiagnosticReportView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// DiagnosticReportView.swift
// LoopSupportKitUI
//
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import SwiftUI

/// Shows the diagnostic report as text, with a button to share it as a file.
struct DiagnosticReportView: View {
let generateReport: () async -> String

@State private var report: String?
@State private var reportFile: ReportFile?

private struct ReportFile: Identifiable {
let url: URL
var id: URL { url }
}

var body: some View {
Group {
if let report {
ReportTextView(text: report)
.ignoresSafeArea(edges: .bottom)
} else {
ProgressView(LocalizedString("Preparing Report…", comment: "Shown while the diagnostic report is generated"))
}
}
.navigationTitle(LocalizedString("Diagnostic Report", comment: "Title of the diagnostic report viewer"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: share) {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel(LocalizedString("Share", comment: "Button to share the diagnostic report"))
.disabled(report == nil)
}
}
.sheet(item: $reportFile, onDismiss: removeReportFile) { file in
ActivityView(activityItems: [file.url])
}
.task {
if report == nil {
report = "Generated: \(Date())\n\n" + (await generateReport())
}
}
}

private func share() {
guard let report else {
return
}
let name = "Loop Report \(ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: [.withSpaceBetweenDateAndTime, .withInternetDateTime])).md"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(name.replacingOccurrences(of: ":", with: "-"))
if (try? report.write(to: url, atomically: true, encoding: .utf8)) != nil {
reportFile = ReportFile(url: url)
}
}

private func removeReportFile() {
if let url = reportFile?.url {
try? FileManager.default.removeItem(at: url)
}
}
}

// A UITextView keeps scrolling smooth for reports of several hundred kilobytes
private struct ReportTextView: UIViewRepresentable {
let text: String

func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.dataDetectorTypes = []
textView.adjustsFontForContentSizeCategory = true
textView.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .monospacedSystemFont(ofSize: 12, weight: .regular))
textView.textContainerInset = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 8)
textView.text = text
return textView
}

func updateUIView(_ textView: UITextView, context: Context) {
if textView.text != text {
textView.text = text
}
}
}

private struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]

func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}

func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
69 changes: 6 additions & 63 deletions LoopSupportKitUI/IssueReport/ReportProblemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ import LoopKitUI
struct ReportProblemView: View {
let delegate: SupportUIDelegate?

@State private var isPreparingReport = false
@State private var reportFile: ReportFile?
@State private var writtenReportURL: URL?

private struct ReportFile: Identifiable {
let url: URL
var id: URL { url }
}

var body: some View {
List {
Section {
Expand All @@ -33,20 +24,13 @@ struct ReportProblemView: View {
)
}

if delegate != nil {
Button(action: shareDiagnosticReport) {
HStack {
option(
LocalizedString("Share a Diagnostic Report", comment: "Report a problem option: share the diagnostic report yourself"),
detail: LocalizedString("Save the report or send it to someone helping you, for example on Zulip or Facebook. It contains health data such as glucose, insulin, carbs and your therapy settings.", comment: "Report a problem option detail: share the diagnostic report yourself")
)
if isPreparingReport {
Spacer()
ProgressView()
}
}
if let delegate {
NavigationLink(destination: DiagnosticReportView(generateReport: { await delegate.generateIssueReport() })) {
option(
LocalizedString("Share a Diagnostic Report", comment: "Report a problem option: share the diagnostic report yourself"),
detail: LocalizedString("View the report, then save it or send it to someone helping you, for example on Zulip or Facebook. It contains health data such as glucose, insulin, carbs and your therapy settings.", comment: "Report a problem option detail: share the diagnostic report yourself")
)
}
.disabled(isPreparingReport)
}

Button(action: { delegate?.openURL(url: URL(string: "https://github.com/LoopKit/Loop/issues")!) }) {
Expand All @@ -69,9 +53,6 @@ struct ReportProblemView: View {
.listStyle(.insetGrouped)
.navigationTitle(LocalizedString("Report a Problem", comment: "Title of the report a problem screen"))
.navigationBarTitleDisplayMode(.inline)
.sheet(item: $reportFile, onDismiss: removeReportFile) { file in
ActivityView(activityItems: [file.url])
}
}

private func option(_ title: String, detail: String) -> some View {
Expand All @@ -84,34 +65,6 @@ struct ReportProblemView: View {
}
.padding(.vertical, 4)
}

private func shareDiagnosticReport() {
guard let delegate else {
return
}
isPreparingReport = true
Task { @MainActor in
let date = Date()
let report = "Generated: \(date)\n\n" + (await delegate.generateIssueReport())
let name = "Loop Report \(ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withSpaceBetweenDateAndTime, .withInternetDateTime])).md"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(name.replacingOccurrences(of: ":", with: "-"))
do {
try report.write(to: url, atomically: true, encoding: .utf8)
writtenReportURL = url
reportFile = ReportFile(url: url)
} catch {
reportFile = nil
}
isPreparingReport = false
}
}

private func removeReportFile() {
if let url = writtenReportURL {
try? FileManager.default.removeItem(at: url)
}
writtenReportURL = nil
}
}

struct ReportProblemMenuItem: View {
Expand Down Expand Up @@ -142,16 +95,6 @@ struct ReportProblemModal: View {
}
}

private struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]

func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}

func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

private extension SupportInfoProvider {
func generateIssueReport() async -> String {
await withCheckedContinuation { continuation in
Expand Down
7 changes: 6 additions & 1 deletion LoopSupportKitUI/IssueReport/SendIssueReportView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,14 @@ struct SendIssueReportView: View {
}
}

if generateDiagnosticReport != nil {
if let generateDiagnosticReport {
Section(footer: Text(LocalizedString("Attaches Loop's issue report as a text file. It contains health data such as recent glucose, insulin, carbs and your therapy settings.", comment: "Footer for the include diagnostic report toggle"))) {
Toggle(LocalizedString("Include Diagnostic Report", comment: "Toggle for attaching the diagnostic report to an issue report"), isOn: $includeDiagnosticReport)
if includeDiagnosticReport {
NavigationLink(destination: DiagnosticReportView(generateReport: generateDiagnosticReport)) {
Text(LocalizedString("View Diagnostic Report", comment: "Button to view the diagnostic report attached to an issue report"))
}
}
}
}

Expand Down
18 changes: 15 additions & 3 deletions LoopSupportKitUI/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@
"Detailed logs as files. Only needed when a developer asks for them." : {
"comment" : "Report a problem option detail: export the critical event logs"
},
"Diagnostic Report" : {
"comment" : "Title of the diagnostic report viewer"
},
"Do not share any data." : {
"comment" : "Description in UsageDataPrivacyPreferenceView for no sharing",
"localizations" : {
Expand Down Expand Up @@ -565,6 +568,9 @@
"Open a GitHub Issue" : {
"comment" : "Report a problem option: open the public GitHub issues page"
},
"Preparing Report…" : {
"comment" : "Shown while the diagnostic report is generated"
},
"Private. Describe the problem and attach screenshots and a diagnostic report. Only the LoopKit development team can see it." : {
"comment" : "Report a problem option detail: submit an issue report to the developers"
},
Expand All @@ -580,15 +586,15 @@
"Reports go to the LoopKit development team and are visible only to them. Your IP address is used only to limit how often reports can be sent and is never stored." : {
"comment" : "Privacy footer for issue reports"
},
"Save the report or send it to someone helping you, for example on Zulip or Facebook. It contains health data such as glucose, insulin, carbs and your therapy settings." : {
"comment" : "Report a problem option detail: share the diagnostic report yourself"
},
"Send to Developers" : {
"comment" : "Title of the issue report screen"
},
"Send to the Loop Developers" : {
"comment" : "Report a problem option: submit an issue report to the developers"
},
"Share" : {
"comment" : "Button to share the diagnostic report"
},
"Share a Diagnostic Report" : {
"comment" : "Report a problem option: share the diagnostic report yourself"
},
Expand Down Expand Up @@ -1026,6 +1032,12 @@
}
}
},
"View Diagnostic Report" : {
"comment" : "Button to view the diagnostic report attached to an issue report"
},
"View the report, then save it or send it to someone helping you, for example on Zulip or Facebook. It contains health data such as glucose, insulin, carbs and your therapy settings." : {
"comment" : "Report a problem option detail: share the diagnostic report yourself"
},
"You can optionally choose to share usage data with Loop developers to improve Loop. Sharing is not required, and Loop will function fully no matter which option you choose. Usage data will not be shared with third parties." : {
"comment" : "Main summary text for UsageDataPrivacyPreferenceView",
"localizations" : {
Expand Down