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 {