Files
youle_app_ios_v2/ylgamehall/Source/SDK/WeChat/WeChatManager.swift
T
joywayerandClaude Opus 4.7 a060f29070 微信授权诊断 print:定位 H5 调用 → 拉起微信卡在哪一步
H5 调 accreditlogin 仍无反应。加临时 print 排查链路:

AccreditLoginHandler:
- 入口"H5 调用命中 " → 确认 WVJB 协议握手 + handler 注册 OK
- canImport(WechatOpenSDK) 真值 → 确认 SDK 是否真链接进了 binary
- Task @MainActor 入口 → 确认 actor hop 没卡死
- 失败分支区分用户取消(errCode -2)/ 其它

WeChatManager.authorize():
- isWXAppInstalled() → 用户手机是否真装了微信
- getVersion() → 加载的 SDK 版本字符串(验证链接的 framework)
- topmostViewController() nil 检测 → present 上下文是否拿到
- sendAuthReq completion success=true/false → SDK 是否成功拉起微信 app

跑一次 H5 调 accreditlogin 看 Xcode console 输出序列,按 print 出现/缺失
精确定位卡点(H5 没调进来 / SDK 未链接 / VC 拿不到 / SDK 拉起失败)。
排查完成后可删除 print。

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

262 lines
10 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)
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 直接 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)
}