Files
youle_app_ios_v2/ylgamehall/Source/SDK/WeChat/WeChatManager.swift
T
joywayerandClaude Opus 4.7 624dd93c65 修复微信授权无响应:authorize 改用 sendAuthReq:viewController:delegate:
H5 调 accreditlogin 后无反应。对照 daoqi msext 找到根因:

msext NewRootVC.m → WXApiRequestHandler.m:sendAuthRequestScope: 用的是
`[WXApi sendAuthReq:req viewController:vc delegate:[WXApiManager sharedManager]]`
(新版 SDK 2.x 推荐路径,传 viewController + delegate)。

而 ylgamehall 之前用 `WXApi.send(req)`——旧版 1.x 方法,新版头里已无声明,
不传 viewController(SDK 无法弹起微信 modal context)+ 不传 delegate
(SDK 不知道往哪发回包)= H5 调了无反应。

修复:
- authorize:改用 `WXApi.sendAuthReq(_:viewController:delegate:completion:)`,
  传当前最顶层 VC(新增 topmostViewController() helper:connectedScenes →
  foregroundActive windowScene → keyWindow.rootVC → 穿过 presented / nav.visible)
  + self(WXApiDelegate)。completion 处理拉起失败立即 fail continuation。
- shareLink / shareImage:从旧 send 改新 `WXApi.send(_:completion:)`
  (sendReq:completion: 的 Swift 桥名)。分享回包仍由 SceneDelegate
  openURL → WXApi.handleOpen(url:delegate:WeChatManager.shared) → onResp
  路径接收,不需要在 send 时再传 delegate。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:33:58 +08:00

255 lines
9.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// WeChatManager.swift
// ylgamehall
//
// 微信 SDK 持久 delegate + authorize + share 异步包装。
// 把 WXApiDelegate 的回调 → async/awaitstate UUID 配对多发起方。
//
// Design §8.5msext 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 continuationstate UUID 配对)
private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:]
/// 待 resolve 的 share continuationFIFO 串行 — 微信回包不带配对 ID)
private var sharePending: [CheckedContinuation<Void, Error>] = []
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)
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 {
authPending.removeValue(forKey: state)
cont.resume(throwing: WeChatError.authFailed(-3))
return
}
WXApi.sendAuthReq(req, viewController: vc, delegate: self) { [weak self] success in
// 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
throw WeChatError.sdkNotLinked
#endif
}
/// 取当前最顶层 ViewController(含 presented / nav.visible),供微信 SDK 作为
/// modal present 的上下文。msext 直接 selfgameController/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<Void, Error>) 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 这里不需要再传 delegatecompletion=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<Void, Error>) in
sharePending.append(cont)
let media = WXImageObject()
media.imageData = img.imageData
let message = WXMediaMessage()
// 图片分享不需要 title / descriptionmsext 字面:传 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 这里不需要再传 delegatecompletion=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)
}