import Foundation import UIKit import XuqmCoreSDK import XuqmWebViewSDK import XuqmBugCollectSDK /// H5 本地存储的 UserDefaults suite 文件名,与 Update SDK 隔离 private let H5StorageSuite = "szyx_h5_storage" /// H5 能感知的最高 App API 版本 private let APP_API_VERSION = 4 // MARK: - Message Dispatcher @MainActor func handleWebMessage( data: String, onStatusBarColorChanged: ((UIColor) -> Void)? = nil, onNavBarChanged: ((String, UIColor?, UIColor?, [UIColor]?, Float, String?) -> Void)? = nil, onCheckUpdateRequested: (() -> Void)? = nil, onOrientationChanged: ((String) -> Void)? = nil ) { do { guard let jsonData = data.data(using: .utf8), let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { return } let model = json["model"] as? String ?? "" let functionId = json["functionId"] as? String ?? "" switch model { case "setTitle": handleSetTitle(json: json, functionId: functionId, onStatusBarColorChanged: onStatusBarColorChanged, onNavBarChanged: onNavBarChanged) case "isWechatInstalled": // WeChat is never installed on iOS (App Store policy) respondToWeb(functionId: functionId, model: model, status: "0", message: "isWechatInstalled:ok", value: ["isWechatInstalled": false]) case "wechatPay": // WeChat Pay is not supported in this build respondToWeb(functionId: functionId, model: model, status: "E50001", message: "当前App不支持微信支付") case "getDeviceInfo": let device = UIDevice.current let deviceInfo: [String: Any] = [ "osName": "iOS", "osVersion": "iOS \(device.systemVersion)", "baseVersion": device.systemVersion, "brand": "Apple", "manufacturer": "Apple", "model": device.model, "marketName": device.name, "deviceId": device.identifierForVendor?.uuidString ?? "", ] respondToWeb(functionId: functionId, model: model, status: "0", message: "getDeviceInfo:ok", value: ["deviceInfo": deviceInfo]) case "login": let userId = json["userId"] as? String ?? "" if !userId.isEmpty { let userSig = (json["userSig"] as? String)?.nonEmpty let name = (json["name"] as? String)?.nonEmpty let avatar = (json["avatar"] as? String)?.nonEmpty XuqmSDK.shared.setUserInfo(userId: userId, nickname: name, avatar: avatar) if let userSig { Task { await XuqmSDK.shared.login(userId: userId, userSig: userSig) } } BugCollectSDK.shared.addBreadcrumb(category: "auth", message: "login userId=\(userId)") } respondOk(functionId: functionId, model: model, message: "login:ok") case "setUserInfo": let userId = ((json["userId"] as? String)?.nonEmpty ?? (json["id"] as? String)?.nonEmpty) ?? "" if !userId.isEmpty { let userSig = (json["userSig"] as? String)?.nonEmpty let name = (json["name"] as? String)?.nonEmpty let avatar = (json["avatar"] as? String)?.nonEmpty XuqmSDK.shared.setUserInfo(userId: userId, nickname: name, avatar: avatar) if let userSig { Task { await XuqmSDK.shared.login(userId: userId, userSig: userSig) } } } respondOk(functionId: functionId, model: model, message: "setUserInfo:ok") case "getAppVersion": let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" respondToWeb(functionId: functionId, model: model, status: "0", message: "getAppVersion:ok", value: ["version": APP_API_VERSION, "appVersion": versionName]) case "checkUpdate": onCheckUpdateRequested?() respondOk(functionId: functionId, model: model, message: "checkUpdate:ok") case "setStorage": let storage = json["storage"] as? [String: Any] let key = (storage?["key"] as? String) ?? "" let value = (storage?["value"] as? String) ?? "" if !key.isEmpty { let defaults = UserDefaults(suiteName: H5StorageSuite) defaults?.set(value, forKey: key) } respondOk(functionId: functionId, model: model, message: "setStorage:ok") case "getStorage": let storage = json["storage"] as? [String: Any] let key = (storage?["key"] as? String) ?? "" let defaultValue = (storage?["defaultValue"] as? String) ?? "" let stored: String if !key.isEmpty, let v = UserDefaults(suiteName: H5StorageSuite)?.string(forKey: key) { stored = v } else { stored = defaultValue } respondToWeb(functionId: functionId, model: model, status: "0", message: "getStorage:ok", value: ["storage": ["key": key, "value": stored]]) case "removeStorage": let key = (json["storage"] as? [String: Any])?["key"] as? String ?? "" if !key.isEmpty { UserDefaults(suiteName: H5StorageSuite)?.removeObject(forKey: key) } respondOk(functionId: functionId, model: model, message: "removeStorage:ok") case "clearStorage": if let d = UserDefaults(suiteName: H5StorageSuite) { d.dictionaryRepresentation().keys.forEach { d.removeObject(forKey: $0) } } respondOk(functionId: functionId, model: model, message: "clearStorage:ok") case "setOrientation": let orientation = json["orientation"] as? String ?? "" onOrientationChanged?(orientation) respondOk(functionId: functionId, model: model, message: "setOrientation:ok") case "apiError": let d = (json["data"] as? [String: Any]) ?? json let url = d["url"] as? String ?? "" let method = d["method"] as? String ?? "" let status = d["status"] as? Int ?? 0 let statusText = d["statusText"] as? String ?? "" let body = d["response"] as? String ?? "" BugCollectSDK.shared.captureError( ApiCallError(method: method, url: url, status: status, statusText: statusText, body: body), tags: ["context": "webViewApi", "url": url, "method": method, "status": status] ) default: respondToWeb(functionId: functionId, model: model, status: "E40001", message: "当前App版本不支持该接口") } } catch { BugCollectSDK.shared.captureError(error, tags: ["context": "handleWebMessage"]) } } // MARK: - setTitle helpers private func handleSetTitle( json: [String: Any], functionId: String, onStatusBarColorChanged: ((UIColor) -> Void)?, onNavBarChanged: ((String, UIColor?, UIColor?, [UIColor]?, Float, String?) -> Void)? ) { let titleObj = json["title"] as? [String: Any] let titleText = titleObj?["titleText"] as? String ?? "" let textColor = parseHexColor(titleObj?["titleTextColor"] as? String) let bgImage = (titleObj?["titleBackgroundImage"] as? String)?.nonEmpty let gradientArr = titleObj?["titleBackgroundGradient"] as? [String] let bgGradient: [UIColor]? let bgColor: UIColor? if let gradientArr, gradientArr.count >= 2 { bgGradient = gradientArr.compactMap { parseHexColor($0) } bgColor = bgGradient?.first } else { bgGradient = nil bgColor = parseHexColor(titleObj?["titleBackgroundColor"] as? String) } let gradientAngle = (titleObj?["titleBackgroundGradientAngle"] as? Double).map { Float($0) } ?? 0 if let bgColor { onStatusBarColorChanged?(bgColor) } onNavBarChanged?(titleText, textColor, bgColor, bgGradient, gradientAngle, bgImage) respondOk(functionId: functionId, model: "setTitle", message: "setTitle:ok") } // MARK: - Response helpers private func respondOk(functionId: String, model: String, message: String) { respondToWeb(functionId: functionId, model: model, status: "0", message: message, value: [:]) } private func respondToWeb( functionId: String, model: String, status: String = "0", message: String, value: [String: Any] = [:] ) { var response: [String: Any] = [ "functionId": functionId, "model": model, "status": status, "message": message, "value": value, ] guard let data = try? JSONSerialization.data(withJSONObject: response), let json = String(data: data, encoding: .utf8) else { return } let js = "(function(){window.SZYX_YWX_WebViewBridge&&window.SZYX_YWX_WebViewBridge.onMessage(\(json));})()" postJSToWebView(js) } // MARK: - Color helpers func parseHexColor(_ hex: String?) -> UIColor? { guard var h = hex?.trimmingCharacters(in: .whitespaces), !h.isEmpty else { return nil } if h.hasPrefix("#") { h = String(h.dropFirst()) } guard h.count == 6 || h.count == 8 else { return nil } var value: UInt64 = 0 Scanner(string: h).scanHexInt64(&value) if h.count == 6 { return UIColor( red: CGFloat((value >> 16) & 0xFF) / 255, green: CGFloat((value >> 8) & 0xFF) / 255, blue: CGFloat( value & 0xFF) / 255, alpha: 1 ) } else { return UIColor( red: CGFloat((value >> 24) & 0xFF) / 255, green: CGFloat((value >> 16) & 0xFF) / 255, blue: CGFloat((value >> 8) & 0xFF) / 255, alpha: CGFloat( value & 0xFF) / 255 ) } } func isLightColor(_ color: UIColor) -> Bool { var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0 color.getRed(&r, green: &g, blue: &b, alpha: nil) return 0.299 * r + 0.587 * g + 0.114 * b > 0.5 } // MARK: - JS posting func postJSToWebView(_ js: String) { // Forward via XWebViewBridge to the active WKWebView Task { @MainActor in XWebViewBridge.shared.postMessageToWeb(js) } } // MARK: - Supporting types private struct ApiCallError: Error { let method: String let url: String let status: Int let statusText: String let body: String var localizedDescription: String { "API \(method) \(url) → \(status) \(statusText)" } } private extension String { var nonEmpty: String? { isEmpty ? nil : self } }