// // FriendsShareHandler.swift // ylgamehall // // H5 → Native handler:friendsSharetypeUrlToptitleDescript — 分享 // 契约:docs/H5-Native-Contract.md §3.1 [2] + §3.2 [11]sharesuccess // // **页面 mode 区分**(与原 msext 两个 ViewController 各注册一份且行为不同对齐): // - `.lobby` 对应 NewRootVC.m:直发微信,3 type 分支(链接 / 截图 / 远端图) // - `.subGame` 对应 gameController.m 活路径:SharePanel 三选一 + 朋友圈兜底 // // 公共流程(无论 mode): // 1. responseCallback("sharefriend") // 2. 立即 bridge.call("sharesuccess", { success:"2", type: }) // (**有意为之**:调用分享接口即视为成功;URL Scheme / 剪贴板路径本就无返回) // 3. 按 mode 异步 fire-and-forget 拉起 UI // // 各平台 SDK 依赖: // - 微信:通过 WeChatManager(已接入 1.8.2 SDK) // - QQ + 抖音:不依赖 SDK,走 URL Scheme + 剪贴板(CLAUDE.md ADR-006) // import Foundation import os.log @MainActor public enum FriendsShareHandler { private static let log = Logger(subsystem: "ylgamehall", category: "FriendsShare") /// 注册时机的页面 mode。 public enum Mode: Sendable { /// 大厅页:H5 在 WebContainer 调 friendsShare 时直发微信 case lobby /// 子游戏页:H5 在 SubGame 调 friendsShare 时弹 SharePanel 或朋友圈兜底 case subGame } public static func register(on bridge: any BridgeProtocol, mode: Mode) { // 【2】friendsSharetypeUrlToptitleDescript // 入参:sharefriend / sharetype / type / webpageUrl / title / description // cb: "sharefriend"(msext 字面,沿用) // 反向 callback:sharesuccess({ success: "2", type: sharefriend 原值 }) bridge.register("friendsSharetypeUrlToptitleDescript") { [weak bridge] data, callback in // 1. responseCallback —— msext 字面 "sharefriend" callback?(.string("sharefriend")) await MainActor.run { // 0. 调试日志:H5 传入的原始 JSON(含未声明字段,便于排查 H5 端字段漂移) Self.log.debug("← H5 friendsShare mode=\(String(describing: mode), privacy: .public) raw=\(Self.describe(data), privacy: .public)") guard let obj = data?.asObject else { Self.log.error("friendsShare: data 不是 object,跳过分发") return } let content = ShareContent( // sharefriend / sharetype / type 实际 H5 传 number(如 1/2 而非 "1"/"2"), // 必须用 asLooseString 兼容(与 mediaTypeAudio.user 同款问题,commit b058990)。 // 对齐原 msext ObjC 行为:[NSNumber intValue] 与 [NSString intValue] 都得到整数。 // 用 asString 会拿不到值 → 整路由跑偏到 default 分支。 sharefriend: obj["sharefriend"]?.asLooseString ?? "", sharetype: obj["sharetype"]?.asLooseString ?? "", type: obj["type"]?.asLooseString ?? "", // 下面 3 项 H5 始终传 string,保持 asString webpageUrl: obj["webpageUrl"]?.asString ?? "", title: obj["title"]?.asString ?? "", desc: obj["description"]?.asString ?? "" ) // 调试日志:解析后的 ShareContent 各字段(带语义注释方便定位行为分支) Self.log.debug(""" friendsShare parsed: sharefriend = "\(content.sharefriend, privacy: .public)" (1=好友/SharePanel, 2/其他=朋友圈) sharetype = "\(content.sharetype, privacy: .public)" (1/2=微信, 3=闲聊;新外壳忽略) type = "\(content.type, privacy: .public)" (1=链接, 2=截图, 其他=远端图) webpageUrl = "\(content.webpageUrl, privacy: .public)" title = "\(content.title, privacy: .public)" description = "\(content.desc, privacy: .public)" """) // 2. 立即触发 sharesuccess —— 有意为之:调用即视为成功 bridge?.call( "sharesuccess", data: .object([ "success": .string("2"), "type": .string(content.sharefriend) ]), callback: nil ) Self.log.debug("→ H5 sharesuccess fired: success=\"2\", type=\"\(content.sharefriend, privacy: .public)\"") // 3. fire-and-forget 拉起真实分享 UI(按页面 mode 选路径) switch mode { case .lobby: Self.log.debug("dispatch → lobby (直发微信 3 type 分支)") ShareCenter.dispatchLobby(content) case .subGame: Self.log.debug("dispatch → subGame (SharePanel 或朋友圈兜底)") ShareCenter.dispatchSubGame(content) } } } } /// 把 BridgeData 整体序列化为 JSON 字符串方便日志输出(含 H5 传入的所有字段, /// 不限于上面解析的 6 项 —— 万一 H5 偷偷加了字段也能看到)。 private static func describe(_ data: BridgeData?) -> String { guard let data else { return "" } let raw = data.jsonObject if let bytes = try? JSONSerialization.data( withJSONObject: raw, options: [.fragmentsAllowed, .sortedKeys] ), let s = String(data: bytes, encoding: .utf8) { return s } return String(describing: raw) } }