learning_ai_common_plat/packages/swift-platform-sdk/Sources/BLFeedbackClient.swift
saravanakumardb1 85d9356a19 feat(platform-sdk): implement TODO-2 and TODO-3 - Swift and Kotlin feedback clients
- Add BLFeedbackClient.swift with submitFeedback(), captureAndSubmit(), captureScreen()
- Add BLFeedbackClient.kt with FeedbackParams, DeviceContext, screenshot capture
- Include implementation instructions and error handling
- Mirror API structure between Swift and Kotlin SDKs
2026-03-03 07:18:45 -08:00

270 lines
9.2 KiB
Swift

// 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)
}