// // WeChatManager.swift // ylgamehall // // 微信 SDK 持久 delegate + authorize + share 异步包装。 // 把 WXApiDelegate 的回调 → async/await,state UUID 配对多发起方。 // // Design §8.5;msext WXApiManager.m 等价。 // import Foundation import UIKit #if canImport(WechatOpenSDK) import WechatOpenSDK #endif @MainActor public final class WeChatManager: NSObject { public static let shared = WeChatManager() /// 授权 scope(沿用 msext SGDefineInfo.h:104) public static let authScope = "snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact" /// 待 resolve 的 authorize continuation(state UUID 配对) private var authPending: [String: CheckedContinuation] = [:] /// 待 resolve 的 share continuation(FIFO 串行 — 微信回包不带配对 ID) private var sharePending: [CheckedContinuation] = [] public override init() { super.init() } // MARK: - Authorize /// 拉起微信授权(msext NewRootVC.m → WXApiRequestHandler.m 同款路径)。 /// 必须用新版 SDK 2.x 的 `sendAuthReq:viewController:delegate:completion:`: /// - viewController = SDK 拉起微信回来时的 present 上下文(缺失则 SDK 无法弹起) /// - delegate = SDK 弱引用,授权完成由 WXApiDelegate.onResp 回调(与 SceneDelegate /// openURL 路径解耦,授权回包独立由 SDK 直接派给 delegate) /// 旧版 1.x 的 `WXApi.send(req)` 已 deprecated,不传 vc/delegate 会导致 H5 调了无反应。 public func authorize() async throws -> WXAuthCodePayload { #if canImport(WechatOpenSDK) print("[WeChat] authorize() 开始") print("[WeChat] - WXApi.isWXAppInstalled = \(WXApi.isWXAppInstalled())") print("[WeChat] - WXApi.getVersion = \(WXApi.getVersion() ?? "nil")") let state = UUID().uuidString return try await withCheckedThrowingContinuation { cont in authPending[state] = cont let req = SendAuthReq() req.scope = Self.authScope req.state = state guard let vc = Self.topmostViewController() else { print("[WeChat] ⚠️ topmostViewController() 返回 nil,无 present 上下文") authPending.removeValue(forKey: state) cont.resume(throwing: WeChatError.authFailed(-3)) return } print("[WeChat] 调 sendAuthReq vc=\(type(of: vc)) scope=\(Self.authScope) state=\(state)") WXApi.sendAuthReq(req, viewController: vc, delegate: self) { [weak self] success in print("[WeChat] sendAuthReq completion success=\(success)") // success = SDK 是否成功拉起微信 app;失败立刻 fail(如微信未安装、 // URL Scheme 未配置等)。成功后真实回包由 onResp 路径处理。 guard !success else { return } Task { @MainActor in guard let self else { return } if let pending = self.authPending.removeValue(forKey: state) { pending.resume(throwing: WeChatError.authFailed(-3)) } } } } #else print("[WeChat] ⚠️ canImport(WechatOpenSDK)=false") throw WeChatError.sdkNotLinked #endif } /// 取当前最顶层 ViewController(含 presented / nav.visible),供微信 SDK 作为 /// modal present 的上下文。msext 直接 self(gameController/NewRootVC),新外壳 /// AccreditLoginHandler 是 stateless enum,由本方法 resolve。 private static func topmostViewController() -> UIViewController? { let windowScene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first(where: { $0.activationState == .foregroundActive }) ?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first guard let root = windowScene?.windows.first(where: \.isKeyWindow)?.rootViewController ?? windowScene?.windows.first?.rootViewController else { return nil } var top: UIViewController = root while let presented = top.presentedViewController { top = presented } if let nav = top as? UINavigationController, let visible = nav.visibleViewController { return visible } return top } // MARK: - Share public enum ShareScene { case session // 微信好友 case timeline // 朋友圈 } public struct ShareLink: Sendable { public let url: String public let title: String public let desc: String public let thumbnailURL: String? public init(url: String, title: String, desc: String, thumbnailURL: String?) { self.url = url; self.title = title; self.desc = desc; self.thumbnailURL = thumbnailURL } } /// 图片分享载荷(type=2 截图 / type=3 远端图)。 /// imageData 是大图原始字节(JPEG 等),thumb 是缩略图源 — SDK /// `setThumbImage:` 内部把 thumb 压到 32KB。 public struct ShareImage { public let imageData: Data public let thumb: UIImage public init(imageData: Data, thumb: UIImage) { self.imageData = imageData self.thumb = thumb } } /// 链接分享(FIFO 串行)。微信回包不携带配对 ID,连续调时按入队顺序消费。 public func shareLink(_ link: ShareLink, scene: ShareScene) async throws { #if canImport(WechatOpenSDK) try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in sharePending.append(cont) let media = WXWebpageObject() media.webpageUrl = link.url let message = WXMediaMessage() message.title = link.title message.description = link.desc message.mediaObject = media let req = SendMessageToWXReq() req.bText = false req.message = message switch scene { case .session: req.scene = Int32(WXSceneSession.rawValue) case .timeline: req.scene = Int32(WXSceneTimeline.rawValue) } // 分享回包路径:用户在微信操作后由 SceneDelegate.openURLContexts → // WXApi.handleOpen(url:delegate:WeChatManager.shared) → onResp 到达, // 故 sendReq 这里不需要再传 delegate;completion=nil 让拉起失败时 // 不打扰 sharePending 队列(队列由 onResp 的 FIFO 消费)。 // 新版 SDK 2.x `sendReq:completion:` 替代旧 `send:`(后者 deprecated)。 WXApi.send(req, completion: nil) } #else throw WeChatError.sdkNotLinked #endif } /// 图片分享(FIFO 串行,复用 sharePending)。msext gameController.m:637-678 /// type=2 截图 / type=其它 远端图 共用同一条 WXImageObject + setThumbImage 路径。 public func shareImage(_ img: ShareImage, scene: ShareScene) async throws { #if canImport(WechatOpenSDK) try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in sharePending.append(cont) let media = WXImageObject() media.imageData = img.imageData let message = WXMediaMessage() // 图片分享不需要 title / description(msext 字面:传 nil;这里传空串等价) message.mediaObject = media message.setThumbImage(img.thumb) let req = SendMessageToWXReq() req.bText = false req.message = message switch scene { case .session: req.scene = Int32(WXSceneSession.rawValue) case .timeline: req.scene = Int32(WXSceneTimeline.rawValue) } // 分享回包路径:用户在微信操作后由 SceneDelegate.openURLContexts → // WXApi.handleOpen(url:delegate:WeChatManager.shared) → onResp 到达, // 故 sendReq 这里不需要再传 delegate;completion=nil 让拉起失败时 // 不打扰 sharePending 队列(队列由 onResp 的 FIFO 消费)。 // 新版 SDK 2.x `sendReq:completion:` 替代旧 `send:`(后者 deprecated)。 WXApi.send(req, completion: nil) } #else throw WeChatError.sdkNotLinked #endif } // MARK: - Internal: resolve callbacks fileprivate func resolveAuth(state: String, code: String?, errCode: Int) { guard let cont = authPending.removeValue(forKey: state) else { return } if errCode == 0, let code { cont.resume(returning: WXAuthCodePayload(code: code, state: state)) } else { cont.resume(throwing: WeChatError.authFailed(errCode)) } } fileprivate func resolveShare(errCode: Int) { guard !sharePending.isEmpty else { return } let cont = sharePending.removeFirst() if errCode == 0 { cont.resume(returning: ()) } else { cont.resume(throwing: WeChatError.shareFailed(errCode)) } } } // MARK: - WXApiDelegate #if canImport(WechatOpenSDK) extension WeChatManager: WXApiDelegate { public nonisolated func onReq(_ req: BaseReq) { // 我们当前只发起请求,不接受外部 req(微信 → 本 App 的 req 用于公众号回复等场景) } public nonisolated func onResp(_ resp: BaseResp) { // 主线程化:BaseResp 由 SDK 在主线程派发,但保留 MainActor.run 兜底 let errCode = Int(resp.errCode) if let auth = resp as? SendAuthResp { let code = auth.code let state = auth.state ?? "" Task { @MainActor in Self.shared.resolveAuth(state: state, code: code, errCode: errCode) } } else if resp is SendMessageToWXResp { Task { @MainActor in Self.shared.resolveShare(errCode: errCode) } } } } #endif // MARK: - Public payload / error types(无 SDK 依赖,可在 canImport 之外使用) public struct WXAuthCodePayload: Sendable { public let code: String public let state: String } public enum WeChatError: Error, Sendable { case sdkNotLinked case authFailed(Int) case shareFailed(Int) }