diff --git a/Sources/XuqmWebViewSDK/XWebViewStandardHandlers.swift b/Sources/XuqmWebViewSDK/XWebViewStandardHandlers.swift new file mode 100644 index 0000000..4b627b4 --- /dev/null +++ b/Sources/XuqmWebViewSDK/XWebViewStandardHandlers.swift @@ -0,0 +1,176 @@ +import Foundation +import UIKit +import XuqmCoreSDK + +/// JSBridge 标准处理器 +/// 提供 xuqm.* 命名空间的标准桥接方法 +public final class XWebViewStandardHandlers { + + private weak var viewController: UIViewController? + private var onClose: (() -> Void)? + + public init(viewController: UIViewController? = nil, onClose: (() -> Void)? = nil) { + self.viewController = viewController + self.onClose = onClose + } + + /// 处理 JS 消息 + /// - Returns: 是否已处理 + public func handleMessage(_ message: [String: Any]) -> Bool { + guard let action = message["action"] as? String else { + return false + } + + let callbackId = message["callbackId"] as? String + + switch action { + case "xuqm.getDeviceInfo": + handleGetDeviceInfo(callbackId: callbackId) + return true + case "xuqm.getToken": + handleGetToken(callbackId: callbackId) + return true + case "xuqm.getUserInfo": + handleGetUserInfo(callbackId: callbackId) + return true + case "xuqm.closeWebView": + handleCloseWebView() + return true + case "xuqm.showToast": + handleShowToast(message: message) + return true + default: + return false + } + } + + // MARK: - Handlers + + private func handleGetDeviceInfo(callbackId: String?) { + guard let callbackId = callbackId else { return } + + let device = UIDevice.current + let model = device.model + let osVersion = "iOS \(device.systemVersion)" + let platform = "IOS" + let deviceId = device.identifierForVendor?.uuidString ?? "" + + callCallback(callbackId, data: [ + "model": model, + "osVersion": osVersion, + "platform": platform, + "deviceId": deviceId, + ]) + } + + private func handleGetToken(callbackId: String?) { + guard let callbackId = callbackId else { return } + let token = XuqmSDK.shared.token ?? "" + callCallback(callbackId, data: ["token": token]) + } + + private func handleGetUserInfo(callbackId: String?) { + guard let callbackId = callbackId else { return } + let userId = XuqmSDK.shared.currentUserId ?? "" + callCallback(callbackId, data: ["userId": userId]) + } + + private func handleCloseWebView() { + onClose?() + } + + private func handleShowToast(message: [String: Any]) { + guard let msg = message["message"] as? String else { return } + + DispatchQueue.main.async { [weak self] in + guard let vc = self?.viewController else { return } + let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert) + vc.present(alert, animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + alert.dismiss(animated: true) + } + } + } + + // MARK: - Callback + + private func callCallback(_ callbackId: String, data: [String: Any]) { + guard let jsonData = try? JSONSerialization.data(withJSONObject: data), + let jsonString = String(data: jsonData, encoding: .utf8) else { + return + } + + let js = "window[\"\(callbackId)\"](\(jsonString))" + // The WebView will execute this JS through its controller + NotificationCenter.default.post( + name: .xWebViewCallJs, + object: nil, + userInfo: ["js": js] + ) + } + + /// 生成注入的 JS 代码 + public static var injectedJavaScript: String { + return """ + (function() { + window.xuqm = window.xuqm || {}; + + window.xuqm.getDeviceInfo = function(callback) { + var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + window[callbackId] = function(data) { + if (callback) callback(data); + delete window[callbackId]; + }; + window.ReactNativeWebView.postMessage(JSON.stringify({ + action: 'xuqm.getDeviceInfo', + callbackId: callbackId + })); + }; + + window.xuqm.getToken = function(callback) { + var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + window[callbackId] = function(data) { + if (callback) callback(data.token); + delete window[callbackId]; + }; + window.ReactNativeWebView.postMessage(JSON.stringify({ + action: 'xuqm.getToken', + callbackId: callbackId + })); + }; + + window.xuqm.getUserInfo = function(callback) { + var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + window[callbackId] = function(data) { + if (callback) callback(data); + delete window[callbackId]; + }; + window.ReactNativeWebView.postMessage(JSON.stringify({ + action: 'xuqm.getUserInfo', + callbackId: callbackId + })); + }; + + window.xuqm.closeWebView = function() { + window.ReactNativeWebView.postMessage(JSON.stringify({ + action: 'xuqm.closeWebView' + })); + }; + + window.xuqm.showToast = function(message) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + action: 'xuqm.showToast', + message: message + })); + }; + })(); + true; + """ + } +} + +// MARK: - Notification Name + +extension Notification.Name { + static let xWebViewCallJs = Notification.Name("xWebViewCallJs") +} diff --git a/Sources/XuqmWebViewSDK/XWebViewView.swift b/Sources/XuqmWebViewSDK/XWebViewView.swift index 1b13977..7ff5165 100644 --- a/Sources/XuqmWebViewSDK/XWebViewView.swift +++ b/Sources/XuqmWebViewSDK/XWebViewView.swift @@ -77,7 +77,7 @@ true; """# private func buildInjectedJs(_ config: XWebViewConfig) -> String { - dialogOverrideJs + "\n" + (config.injectedJavaScript ?? "") + dialogOverrideJs + "\n" + XWebViewStandardHandlers.injectedJavaScript + "\n" + (config.injectedJavaScript ?? "") } #if canImport(UIKit) @@ -187,6 +187,13 @@ public struct XWebViewView: UIViewRepresentable { config.onMessage?(raw) return } + + // Handle StandardHandlers (xuqm.* actions) + if let action = json["action"] as? String, action.hasPrefix("xuqm.") { + handleStandardAction(action, message: json) + return + } + guard let xwv = json["__xwv"] as? String else { config.onMessage?(raw) return @@ -208,6 +215,60 @@ public struct XWebViewView: UIViewRepresentable { } } + /// 处理 StandardHandlers 动作 + private func handleStandardAction(_ action: String, message: [String: Any]) { + let callbackId = message["callbackId"] as? String + + switch action { + case "xuqm.getDeviceInfo": + handleGetDeviceInfo(callbackId: callbackId) + case "xuqm.getToken": + handleGetToken(callbackId: callbackId) + case "xuqm.getUserInfo": + handleGetUserInfo(callbackId: callbackId) + case "xuqm.closeWebView": + config.onClose?() + case "xuqm.showToast": + // iOS 不支持原生 Toast,可以使用 alert + break + default: + break + } + } + + private func handleGetDeviceInfo(callbackId: String?) { + guard let callbackId = callbackId else { return } + let device = UIDevice.current + let data: [String: Any] = [ + "model": device.model, + "osVersion": "iOS \(device.systemVersion)", + "platform": "IOS", + "deviceId": device.identifierForVendor?.uuidString ?? "", + ] + callCallback(callbackId, data: data) + } + + private func handleGetToken(callbackId: String?) { + guard let callbackId = callbackId else { return } + let token = XuqmSDK.shared.token ?? "" + callCallback(callbackId, data: ["token": token]) + } + + private func handleGetUserInfo(callbackId: String?) { + guard let callbackId = callbackId else { return } + let userId = XuqmSDK.shared.currentUserId ?? "" + callCallback(callbackId, data: ["userId": userId]) + } + + private func callCallback(_ callbackId: String, data: [String: Any]) { + guard let jsonData = try? JSONSerialization.data(withJSONObject: data), + let jsonString = String(data: jsonData, encoding: .utf8) else { + return + } + let js = "window[\"\(callbackId)\"](\(jsonString))" + webView?.evaluateJavaScript(js, completionHandler: nil) + } + private func dispatchDownloadEvent(_ name: String, url: String, extra: String = "") { let escaped = url.jsEscaped let js = "window.dispatchEvent(new CustomEvent('\(name)',{detail:{url:'\(escaped)'\(extra)}}));"