diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
index fa0c548e..fe233326 100644
--- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
+++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
@@ -127,7 +127,7 @@
+ isEnabled = "YES">
String {
+ decoratedMetadataString(Constants.Global.appName, Constants.Global.appVersionString)
+ }
+
+ static func decoratedMetadataData() -> Data {
+ decoratedMetadataData(Constants.Global.appName, Constants.Global.appVersionString)
+ }
+
func decoratedString() -> String {
decoratedString(Constants.Global.appName, Constants.Global.appVersionString)
}
diff --git a/Passepartout/App/L10n/Unlocalized.swift b/Passepartout/App/L10n/Unlocalized.swift
index 3ea97f16..fa0380d2 100644
--- a/Passepartout/App/L10n/Unlocalized.swift
+++ b/Passepartout/App/L10n/Unlocalized.swift
@@ -74,33 +74,54 @@ enum Unlocalized {
static let subject = "\(appName) - Report issue"
- static func body(_ description: String, _ metadata: String) -> String {
- "Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
- }
-
- static let template = "description of the issue: "
+ static let bodySentinel = ""
static let maxLogBytes = UInt64(20000)
enum Filenames {
- static var debugLog: String {
+ static let mime = "text/plain"
+
+ static var appLog: String {
let fmt = DateFormatter()
fmt.dateFormat = "yyyyMMdd-HHmmss"
let iso = fmt.string(from: Date())
- return "debug-\(iso).txt"
+ return "app-\(iso).txt"
}
- static let configuration = "profile.ovpn"
-// static let configuration = "profile.ovpn.txt"
-
- static let template = "description of the issue: "
+ static var tunnelLog: String {
+ let fmt = DateFormatter()
+ fmt.dateFormat = "yyyyMMdd-HHmmss"
+ let iso = fmt.string(from: Date())
+ return "tunnel-\(iso).txt"
+ }
}
- enum MIME {
- static let debugLog = "text/plain"
+ private static func rawBody(_ description: String, _ metadata: String) -> String {
+ "Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
+ }
-// static let configuration = "application/x-openvpn-profile"
- static let configuration = "text/plain"
+ static func body(
+ providerMetadata: ProviderMetadata?,
+ lastUpdate: Date?,
+ purchasedProductIdentifiers: Set?
+ ) -> String {
+ var content: [String] = ["Hi,\n"]
+ content.append(bodySentinel)
+ content.append("\n--\n")
+ content.append(DebugLog.decoratedMetadataString())
+ if let providerMetadata {
+ if let lastUpdate {
+ content.append("Provider: \(providerMetadata.fullName) (last updated: \(lastUpdate))")
+ } else {
+ content.append("Provider: \(providerMetadata.fullName)")
+ }
+ }
+ if let purchasedProductIdentifiers {
+ content.append("Purchased: \(purchasedProductIdentifiers)")
+ }
+ content.append("\n--\n")
+ content.append("Regards")
+ return content.joined(separator: "\n")
}
}
diff --git a/Passepartout/App/Views/DebugLogView.swift b/Passepartout/App/Views/DebugLogView.swift
index 8535274d..d6036a62 100644
--- a/Passepartout/App/Views/DebugLogView.swift
+++ b/Passepartout/App/Views/DebugLogView.swift
@@ -32,6 +32,8 @@ struct DebugLogView: View {
private let url: URL
+ private let shareFilename: String
+
private let timer: AnyPublisher
@State private var logLines: [String] = []
@@ -44,12 +46,11 @@ struct DebugLogView: View {
private let appVersion = Constants.Global.appVersionString
- private let shareFilename = Unlocalized.Issues.Filenames.debugLog
-
- init(title: String, url: URL, refreshInterval: TimeInterval?) {
+ init(title: String, url: URL, filename: String, refreshInterval: TimeInterval?) {
self.title = title
self.url = url
- if let refreshInterval = refreshInterval {
+ shareFilename = filename
+ if let refreshInterval {
timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common)
.autoconnect()
.eraseToAnyPublisher()
diff --git a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift
index 194affc9..511e0f90 100644
--- a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift
+++ b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift
@@ -135,20 +135,11 @@ private extension DiagnosticsView.OpenVPNView {
}
func reportIssueView() -> some View {
- let logURL = vpnManager.debugLogURL(forProtocol: vpnProtocol)
- var metadata: ProviderMetadata?
- var lastUpdate: Date?
- if let name = providerName {
- metadata = providerManager.provider(withName: name)
- lastUpdate = providerManager.lastUpdate(name, vpnProtocol: vpnProtocol)
- }
-
- return ReportIssueView(
+ ReportIssueView(
isPresented: $isReportingIssue,
vpnProtocol: vpnProtocol,
- logURL: logURL,
- providerMetadata: metadata,
- lastUpdate: lastUpdate
+ messageBody: messageBody,
+ logs: logs
)
}
@@ -163,6 +154,40 @@ private extension DiagnosticsView.OpenVPNView {
return cfg.builder(withFallbacks: false)
}
+ var messageBody: String {
+ var providerMetadata: ProviderMetadata?
+ var lastUpdate: Date?
+ if let name = providerName {
+ providerMetadata = providerManager.provider(withName: name)
+ lastUpdate = providerManager.lastUpdate(name, vpnProtocol: vpnProtocol)
+ }
+ return Unlocalized.Issues.body(
+ providerMetadata: providerMetadata,
+ lastUpdate: lastUpdate,
+ purchasedProductIdentifiers: productManager.purchasedProductIdentifiers
+ )
+ }
+
+ var logs: [MailComposerView.Attachment] {
+ var pairs: [(url: URL, filename: String)] = []
+ if let appLogURL {
+ pairs.append((appLogURL, Unlocalized.Issues.Filenames.appLog))
+ }
+ if let tunnelLogURL {
+ pairs.append((tunnelLogURL, Unlocalized.Issues.Filenames.tunnelLog))
+ }
+ return pairs.map {
+ let logContent = $0.url.trailingContent(bytes: Unlocalized.Issues.maxLogBytes)
+ let attachment = DebugLog(content: logContent).decoratedData()
+
+ return MailComposerView.Attachment(
+ data: attachment,
+ mimeType: Unlocalized.Issues.Filenames.mime,
+ fileName: $0.filename
+ )
+ }
+ }
+
var appLogURL: URL? {
Passepartout.shared.logger.logFile
}
@@ -189,9 +214,7 @@ private extension DiagnosticsView.OpenVPNView {
func openReportIssueMailTo() {
let V = Unlocalized.Issues.self
- let body = V.body(V.template, DebugLog(content: "--").decoratedString())
-
- guard let url = URL.mailto(to: V.recipient, subject: V.subject, body: body) else {
+ guard let url = URL.mailto(to: V.recipient, subject: V.subject, body: messageBody) else {
return
}
guard URL.open(url) else {
diff --git a/Passepartout/App/Views/DiagnosticsView.swift b/Passepartout/App/Views/DiagnosticsView.swift
index cbdc81de..7be9e6a6 100644
--- a/Passepartout/App/Views/DiagnosticsView.swift
+++ b/Passepartout/App/Views/DiagnosticsView.swift
@@ -98,6 +98,7 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink(
withTitle: L10n.Diagnostics.Items.AppLog.title,
url: appLogURL,
+ filename: Unlocalized.Issues.Filenames.appLog,
refreshInterval: nil
)
}
@@ -106,19 +107,22 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink(
withTitle: Unlocalized.VPN.vpn,
url: tunnelLogURL,
+ filename: Unlocalized.Issues.Filenames.tunnelLog,
refreshInterval: refreshInterval
)
}
- func navigationLink(withTitle title: String, url: URL?, refreshInterval: TimeInterval?) -> some View {
+ func navigationLink(withTitle title: String, url: URL?, filename: String, refreshInterval: TimeInterval?) -> some View {
NavigationLink(title) {
url.map {
DebugLogView(
title: title,
url: $0,
+ filename: filename,
refreshInterval: refreshInterval
)
}
- }.disabled(url == nil)
+ }
+ .disabled(url == nil)
}
}
diff --git a/Passepartout/App/Views/ReportIssueView.swift b/Passepartout/App/Views/ReportIssueView.swift
index 2af78a1b..b684af5e 100644
--- a/Passepartout/App/Views/ReportIssueView.swift
+++ b/Passepartout/App/Views/ReportIssueView.swift
@@ -37,46 +37,20 @@ struct ReportIssueView: View {
private let messageBody: String
- private let attachments: [MailComposerView.Attachment]
+ private let logs: [MailComposerView.Attachment]
init(
isPresented: Binding,
vpnProtocol: VPNProtocolType,
- logURL: URL?,
- providerMetadata: ProviderMetadata? = nil,
- lastUpdate: Date? = nil
+ messageBody: String,
+ logs: [MailComposerView.Attachment]
) {
_isPresented = isPresented
toRecipients = [Unlocalized.Issues.recipient]
subject = Unlocalized.Issues.subject
-
- let bodyContent = Unlocalized.Issues.template
- var bodyMetadata = "--\n\n"
- bodyMetadata += DebugLog(content: "").decoratedString()
-
- if let metadata = providerMetadata {
- bodyMetadata += "Provider: \(metadata.fullName)\n"
- if let lastUpdate = lastUpdate {
- bodyMetadata += "Last updated: \(lastUpdate)\n"
- }
- bodyMetadata += "\n"
- }
- bodyMetadata += "--"
- messageBody = Unlocalized.Issues.body(bodyContent, bodyMetadata)
-
- var attachments: [MailComposerView.Attachment] = []
- if let logURL = logURL {
- let logContent = logURL.trailingContent(bytes: Unlocalized.Issues.maxLogBytes)
- let attachment = DebugLog(content: logContent).decoratedData()
-
- attachments.append(.init(
- data: attachment,
- mimeType: Unlocalized.Issues.MIME.debugLog,
- fileName: Unlocalized.Issues.Filenames.debugLog
- ))
- }
- self.attachments = attachments
+ self.messageBody = messageBody
+ self.logs = logs
}
var body: some View {
@@ -85,7 +59,7 @@ struct ReportIssueView: View {
toRecipients: toRecipients,
subject: subject,
messageBody: messageBody,
- attachments: attachments
+ attachments: logs
)
}
}
diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift
index 1904c9a4..1460d382 100644
--- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift
+++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift
@@ -48,6 +48,10 @@ public final class ProductManager: NSObject, ObservableObject {
//
+ public var purchasedProductIdentifiers: Set {
+ Set(purchasedFeatures.map(\.rawValue))
+ }
+
private var purchasedAppBuild: Int?
private var purchasedFeatures: Set
diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift
index ef8ef105..f0fd920e 100644
--- a/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift
+++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift
@@ -31,23 +31,23 @@ import AppKit
#endif
extension DebugLog {
- public func decoratedString(_ appName: String, _ appVersion: String) -> String {
+ public static func decoratedMetadataString(_ appName: String, _ appVersion: String) -> String {
let osVersion: String
let deviceType: String?
- #if os(iOS) || os(tvOS)
+#if os(iOS) || os(tvOS)
let device: UIDevice = .current
osVersion = "\(device.systemName) \(device.systemVersion)"
- #if targetEnvironment(macCatalyst)
+#if targetEnvironment(macCatalyst)
deviceType = "\(device.model) (Catalyst)"
- #else
+#else
deviceType = "\(device.model) (\(device.userInterfaceIdiom.debugDescription))"
- #endif
- #else
+#endif
+#else
let os = ProcessInfo().operatingSystemVersion
osVersion = "macOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
deviceType = nil
- #endif
+#endif
var metadata = [
"App: \(appName) \(appVersion)",
@@ -57,10 +57,20 @@ extension DebugLog {
metadata.append("Device: \(deviceType)")
}
- var fullText = metadata.joined(separator: "\n")
- fullText += "\n\n"
- fullText += content
- return fullText
+ return metadata.joined(separator: "\n")
+ }
+
+ public func decoratedString(_ appName: String, _ appVersion: String) -> String {
+ [Self.decoratedMetadataString(appName, appVersion), content]
+ .joined(separator: "\n\n")
+ }
+
+ public static func decoratedMetadataData(_ appName: String, _ appVersion: String) -> Data {
+ guard let data = decoratedMetadataString(appName, appVersion).data(using: .utf8) else {
+ assertionFailure("Could not encode log metadata to UTF8?")
+ return Data()
+ }
+ return data
}
public func decoratedData(_ appName: String, _ appVersion: String) -> Data {