184 lines
5.1 KiB
Swift
184 lines
5.1 KiB
Swift
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
// swiftlint:disable closure_body_length
|
|
|
|
import SwiftUI
|
|
import React
|
|
|
|
struct CrashReportView: View {
|
|
@State private var showCopiedMessage = false
|
|
|
|
let error: EXDevLauncherAppError
|
|
let errorInstance: EXDevLauncherErrorInstance?
|
|
let onDismiss: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
VStack(spacing: 16) {
|
|
clipboardButton
|
|
occuredSection
|
|
reasonSection
|
|
traceSection
|
|
}
|
|
.padding()
|
|
|
|
Spacer()
|
|
|
|
Button(action: onDismiss) {
|
|
Text("Close")
|
|
.font(.headline)
|
|
.foregroundColor(.black)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
#if !os(tvOS)
|
|
.background(Color(.systemGray5))
|
|
#endif
|
|
.cornerRadius(8)
|
|
}
|
|
.padding()
|
|
}
|
|
#if !os(tvOS)
|
|
.background(Color(.systemBackground))
|
|
#endif
|
|
.navigationBarHidden(true)
|
|
}
|
|
|
|
private func copyToClipboard() {
|
|
let crashReport = generateJSONFromCrash()
|
|
#if !os(tvOS)
|
|
UIPasteboard.general.string = crashReport
|
|
|
|
showCopiedMessage = true
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
showCopiedMessage = false
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private var clipboardButton: some View {
|
|
Button(action: copyToClipboard) {
|
|
Text(showCopiedMessage ? "Copied!" : "Tap to Copy Report")
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(showCopiedMessage ? Color.green : Color.black)
|
|
.cornerRadius(8)
|
|
}
|
|
.disabled(showCopiedMessage)
|
|
}
|
|
|
|
private var occuredSection: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Occurred:")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
Spacer()
|
|
}
|
|
Text(formatTimestamp(getCrashDate()))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private var reasonSection: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Reason:")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
Spacer()
|
|
}
|
|
Text(error.message)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(.primary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private var traceSection: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Stack trace:")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
|
|
ScrollView([.horizontal, .vertical]) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if let errorInstance, !errorInstance.stack.isEmpty {
|
|
Text(errorInstance.stack)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(.primary)
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
#if !os(tvOS)
|
|
.textSelection(.enabled)
|
|
#endif
|
|
} else if let stack = error.stack, !stack.isEmpty {
|
|
ForEach(Array(stack.enumerated()), id: \.offset) { _, frame in
|
|
StackFrameView(frame: frame)
|
|
.padding(.vertical, 2)
|
|
}
|
|
} else {
|
|
Text("No stack trace available")
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(.secondary)
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
}
|
|
}
|
|
.padding(.vertical, 8)
|
|
.padding(.horizontal, 12)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.frame(maxHeight: 200)
|
|
#if !os(tvOS)
|
|
.background(Color(.secondarySystemGroupedBackground))
|
|
#endif
|
|
.cornerRadius(8)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private func generateJSONFromCrash() -> String {
|
|
let timestamp = Int64(getCrashDate().timeIntervalSince1970 * 1000)
|
|
|
|
let stackTrace: String
|
|
if let errorInstance, !errorInstance.stack.isEmpty {
|
|
stackTrace = errorInstance.stack
|
|
} else if let stack = error.stack, !stack.isEmpty {
|
|
let stackString = stack.compactMap { frame -> String? in
|
|
guard let methodName = frame.methodName else {
|
|
return nil
|
|
}
|
|
let file = frame.file ?? "Unknown file"
|
|
return "\tat \(methodName) (\(file):\(frame.lineNumber):\(frame.column))"
|
|
}
|
|
.joined(separator: "\n")
|
|
stackTrace = "\(stackString)"
|
|
} else {
|
|
stackTrace = "No stack trace available"
|
|
}
|
|
|
|
let jsonDict: [String: Any] = [
|
|
"timestamp": timestamp,
|
|
"message": error.message,
|
|
"stack": stackTrace
|
|
]
|
|
|
|
do {
|
|
let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
|
return String(data: jsonData, encoding: .utf8) ?? "{}"
|
|
} catch {
|
|
return "{\"error\": \"Failed to serialize crash report\"}"
|
|
}
|
|
}
|
|
|
|
private func getCrashDate() -> Date {
|
|
if let errorInstance {
|
|
return Date(timeIntervalSince1970: TimeInterval(errorInstance.timestamp) / 1000.0)
|
|
}
|
|
return Date()
|
|
}
|
|
}
|
|
// swiftlint:enable closure_body_length
|