// ── Feedback Client ───────────────────────────────────── // Submit user feedback with optional screenshot attachments. // Part of ByteLystPlatformSDK for all iOS/watchOS/macOS apps. // // TODO-2: Full implementation for iOS feedback submission import Foundation import UIKit /// Feedback types supported by the platform. public enum BLFeedbackType: String, Codable, Sendable { case bug case feature case praise case other } /// Device context for debugging. public struct BLDeviceContext: Codable, Sendable { public let osVersion: String public let appVersion: String public let deviceModel: String public let screenResolution: String public let locale: String public init( osVersion: String, appVersion: String, deviceModel: String, screenResolution: String, locale: String ) { self.osVersion = osVersion self.appVersion = appVersion self.deviceModel = deviceModel self.screenResolution = screenResolution self.locale = locale } /// Auto-detect current device context. public static func current() -> BLDeviceContext { let device = UIDevice.current let screen = UIScreen.main let bounds = screen.bounds let scale = screen.scale return BLDeviceContext( osVersion: device.systemVersion, appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown", deviceModel: device.model, screenResolution: "\(Int(bounds.width * scale))x\(Int(bounds.height * scale))", locale: Locale.current.identifier ) } } /// Screenshot content types. public enum BLScreenshotContentType: String, Codable, Sendable { case png = "image/png" case jpeg = "image/jpeg" case webp = "image/webp" } /// Response from feedback SAS endpoint. public struct BLFeedbackSASResponse: Codable, Sendable { public let blobPath: String public let uploadUrl: String public let expiresIn: Int public let maxSizeBytes: Int } /// Response from feedback submission. public struct BLFeedbackResponse: Codable, Sendable { public let id: String public let productId: String public let userId: String public let type: String public let title: String public let status: String public let createdAt: String public let screenshotBlobPath: String? } /// Parameters for submitting feedback. public struct BLFeedbackParams: Sendable { public let type: BLFeedbackType public let title: String public let body: String? public let screen: String? public let rating: Int? public let screenshot: (data: Data, contentType: BLScreenshotContentType)? public let deviceContext: BLDeviceContext? public init( type: BLFeedbackType, title: String, body: String? = nil, screen: String? = nil, rating: Int? = nil, screenshot: (data: Data, contentType: BLScreenshotContentType)? = nil, deviceContext: BLDeviceContext? = nil ) { self.type = type self.title = title self.body = body self.screen = screen self.rating = rating self.screenshot = screenshot self.deviceContext = deviceContext } } /// Feedback client for submitting user feedback with screenshots. /// TODO-2: Full implementation public final class BLFeedbackClient { private let config: BLPlatformConfig private let client: BLPlatformClient private let blobClient: BLBlobClient public init(config: BLPlatformConfig, client: BLPlatformClient, blobClient: BLBlobClient) { self.config = config self.client = client self.blobClient = blobClient } // MARK: - Submit Feedback /// Submit feedback with optional screenshot. /// /// Flow: /// 1. If screenshot provided, upload to blob storage /// 2. Submit feedback with screenshot metadata /// /// TODO-2: Full implementation public func submitFeedback(_ params: BLFeedbackParams) async throws -> BLFeedbackResponse { var screenshotMeta: (blobPath: String, contentType: String, sizeBytes: Int)? // Step 1: Handle screenshot upload if provided if let screenshot = params.screenshot { // Get SAS URL for upload let sas = try await generateSASURL(contentType: screenshot.contentType) // Upload screenshot try await uploadScreenshot(data: screenshot.data, sasURL: sas.uploadUrl, contentType: screenshot.contentType.rawValue) screenshotMeta = ( blobPath: sas.blobPath, contentType: screenshot.contentType.rawValue, sizeBytes: screenshot.data.count ) } // Step 2: Submit feedback var body: [String: Any] = [ "type": params.type.rawValue, "title": params.title, ] if let bodyText = params.body { body["body"] = bodyText } if let screen = params.screen { body["screen"] = screen } if let rating = params.rating { body["rating"] = rating } if let meta = screenshotMeta { body["screenshotBlobPath"] = meta.blobPath body["screenshotContentType"] = meta.contentType body["screenshotSizeBytes"] = meta.sizeBytes } if let context = params.deviceContext { body["deviceContext"] = [ "osVersion": context.osVersion, "appVersion": context.appVersion, "deviceModel": context.deviceModel, "screenResolution": context.screenResolution, "locale": context.locale, ] } throw BLFeedbackError.notImplemented( "submitFeedback body encoding and API call not yet implemented. " + "Use client.request(path: \"/api/feedback\", method: \"POST\", body: body)" ) } /// Capture screenshot and submit feedback in one operation. /// /// TODO-2: Implement using UIApplication.shared.windows or UIScreen public func captureAndSubmit( type: BLFeedbackType, title: String, body: String? = nil ) async throws -> BLFeedbackResponse { throw BLFeedbackError.notImplemented( "captureAndSubmit not yet implemented.\n\n" + "To implement:\n" + "1. Use UIGraphicsImageRenderer or UIScreen to capture\n" + "2. Convert UIImage to Data (PNG/JPEG)\n" + "3. Call submitFeedback with captured data\n\n" + "Example:\n" + "let window = UIApplication.shared.windows.first\n" + "UIGraphicsBeginImageContext(window.bounds.size)\n" + "window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)\n" + "let image = UIGraphicsGetImageFromCurrentImageContext()\n" + "UIGraphicsEndImageContext()" ) } // MARK: - Screenshot Capture /// Capture current screen as UIImage. /// /// TODO-2: Full implementation public func captureScreen() async throws -> UIImage { throw BLFeedbackError.notImplemented( "captureScreen not yet implemented. Use UIScreen or UIApplication.shared.windows" ) } /// Capture specific UIView as UIImage. /// /// TODO-2: Full implementation public func captureView(_ view: UIView) async throws -> UIImage { throw BLFeedbackError.notImplemented( "captureView not yet implemented. Use UIGraphicsImageRenderer or drawHierarchy" ) } // MARK: - Private private func generateSASURL(contentType: BLScreenshotContentType) async throws -> BLFeedbackSASResponse { let body = ["contentType": contentType.rawValue] return try await client.request( path: "/api/feedback/sas", method: "POST", body: body, responseType: BLFeedbackSASResponse.self ) } private func uploadScreenshot(data: Data, sasURL: String, contentType: String) async throws { guard let url = URL(string: sasURL) else { throw BLNetworkError.invalidURL(sasURL) } var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue(contentType, forHTTPHeaderField: "Content-Type") request.setValue("BlockBlob", forHTTPHeaderField: "x-ms-blob-type") request.httpBody = data request.timeoutInterval = 60 let (_, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { throw BLNetworkError.httpError( statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0, message: "Screenshot upload failed" ) } } } /// Errors specific to feedback operations. public enum BLFeedbackError: Error, Sendable { case notImplemented(String) case uploadFailed(String) case invalidScreenshot case sizeLimitExceeded(Int, max: Int) }